diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..467817bb3e358c3674e1754461fd1befeff94ffb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Node dependencies & build outputs +frontend/node_modules +frontend/dist + +# Python virtual environments +backend/.venv +venv +ENV + +# Caches / bytecode +**/__pycache__ +*.py[cod] +.pytest_cache +.mypy_cache +.pytype +.pyright + +# Git & VCS +.git +.gitignore + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# Editors / OS +.idea +.vscode +*.code-workspace +Thumbs.db +.DS_Store + +# Misc +coverage* +dist +build \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..90098343e34770145a7f853015ded090a8191893 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Supabase Configuration +SUPABASE_URL=your_supabase_project_url_here +SUPABASE_KEY=your_supabase_anon_key_here + +# Example: +# SUPABASE_URL=https://your-project-id.supabase.co +# SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + +# Other environment variables +# Add other environment variables your app needs here \ No newline at end of file diff --git a/.github/workflows/run-rd-pipeline.yml b/.github/workflows/run-rd-pipeline.yml new file mode 100644 index 0000000000000000000000000000000000000000..b5d6d6e98ae34d25d8b4c67628340fe997094c89 --- /dev/null +++ b/.github/workflows/run-rd-pipeline.yml @@ -0,0 +1,56 @@ +name: Run Reddit Pipeline + +on: + schedule: + - cron: '0 3 * * *' # Runs daily at 03:00 UTC + workflow_dispatch: + inputs: + user_input_override: + description: 'Optional raw text to process instead of Reddit fetch' + required: false + default: '' + +jobs: + run-pipeline: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PYTHONUNBUFFERED: '1' + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + BRIGHTDATA_API_KEY: ${{ secrets.BRIGHTDATA_API_KEY }} + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + REDDIT_USER_AGENT: 'Mozilla/5.0' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-${{ hashFiles('backend/requirements.txt') }} + restore-keys: | + pip-${{ runner.os }}- + + - name: Install dependencies + working-directory: backend + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run rd_pipeline (Reddit fetch mode or manual override) + working-directory: backend + env: + USER_INPUT_OVERRIDE: ${{ github.event.inputs.user_input_override || '' }} + run: python rd_pipeline_bdata.py + + - name: Summarize run + if: always() + run: | + echo 'Run complete at:' $(date -u) diff --git a/.github/workflows/sync-to-huggingface.yml b/.github/workflows/sync-to-huggingface.yml new file mode 100644 index 0000000000000000000000000000000000000000..f94ec6e57785e87cdcd159fccc6351837a84b0ef --- /dev/null +++ b/.github/workflows/sync-to-huggingface.yml @@ -0,0 +1,109 @@ +name: Sync to Hugging Face Hub + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +env: + HF_REPO_TYPE: space + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install huggingface_hub + run: | + python -m pip install --upgrade pip + pip install huggingface_hub + + - name: Validate secrets + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_REPO: ${{ secrets.HF_REPO }} + run: | + if [ -z "$HF_TOKEN" ]; then + echo "HF_TOKEN secret is not set" + exit 1 + fi + if [ -z "$HF_REPO" ]; then + echo "HF_REPO secret is not set (should be like: username/repo-name)" + exit 1 + fi + + - name: Create or verify Hugging Face repo + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_REPO: ${{ secrets.HF_REPO }} + HF_REPO_TYPE: ${{ env.HF_REPO_TYPE }} + run: | + python3 -c ' + import os + from huggingface_hub import HfApi, create_repo + + api = HfApi() + repo_id = os.environ["HF_REPO"] + repo_type = os.environ.get("HF_REPO_TYPE", "model") + token = os.environ["HF_TOKEN"] + + try: + api.repo_info(repo_id=repo_id, repo_type=repo_type, token=token) + print(f"{repo_type.capitalize()} {repo_id} exists.") + except: + print(f"Creating {repo_type} {repo_id}...") + create_repo(repo_id=repo_id, token=token, repo_type=repo_type, private=False, exist_ok=True) + print(f"{repo_type.capitalize()} {repo_id} created.") + ' + + - name: Mirror to Hugging Face + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_REPO: ${{ secrets.HF_REPO }} + HF_REPO_TYPE: ${{ env.HF_REPO_TYPE }} + run: | + set -e + + # Create temp directory + TMP_DIR=$(mktemp -d) + echo "Using temp directory: $TMP_DIR" + + # Copy files (excluding .git) + cp -r . "$TMP_DIR" + rm -rf "$TMP_DIR/.git" + cd "$TMP_DIR" + + # Initialize git + git init + git config user.name "github-actions" + git config user.email "github-actions@users.noreply.github.com" + + # Set remote URL based on repo type + if [ "$HF_REPO_TYPE" = "space" ]; then + REMOTE_URL="https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_REPO}" + else + REMOTE_URL="https://user:${HF_TOKEN}@huggingface.co/${HF_REPO}" + fi + + git remote add origin "$REMOTE_URL" + git add . + git commit -m "Sync from GitHub $(date -u)" + git branch -M main + git push origin main --force + + echo "Successfully synced to Hugging Face!" + + - name: Summary + run: echo "Sync completed successfully" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ebeb28e74553ff56a8b0133cfdf77da539a32507 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# --- OS / Editor --- +.DS_Store +Thumbs.db +desktop.ini +.idea/ +.vscode/ +*.code-workspace + +# --- Root caches --- +.pytest_cache/ +.mypy_cache/ +.pytype/ +.pyright/ +.coverage +coverage.xml +htmlcov/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +*.tsbuildinfo + +# --- Python (backend) --- +backend/.venv/ +backend/.env +backend/images/ +venv/ +ENV/ +env/ +**/__pycache__/ +**/*.py[cod] +*.pyd +*.pyo +*.so +*.egg-info/ +.eggs/ + +# --- Node / Vite (frontend) --- +frontend/node_modules/ +frontend/dist/ +frontend/.env +frontend/.env.local +frontend/.env.*.local +# Optional: local tooling caches +frontend/.cache/ + +# --- Misc build outputs (root) --- +/dist/ +/build/ + +# --- OS sync artifacts --- +~$* +*.tmp +*.temp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..9920acd70a4cba263bd34f6c7acae43f4cad77a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# -------- Stage 1: Build frontend (Vite + React) -------- +FROM node:20-alpine AS frontend-builder +WORKDIR /frontend + +# Install deps first (better layer caching) +COPY frontend/package*.json ./ +COPY frontend/tsconfig.json frontend/vite.config.* frontend/index.html ./ +RUN npm install + +# Copy source and build +COPY frontend/src ./src +RUN npm run build + +# -------- Stage 2: Backend runtime (FastAPI + Uvicorn) -------- +FROM python:3.12-slim AS runtime +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Install backend dependencies +COPY backend/requirements.txt ./backend/requirements.txt +RUN pip install --no-cache-dir -r backend/requirements.txt + +# Copy backend code +COPY backend ./backend + +# Copy built frontend into expected path (/app/frontend/dist) +COPY --from=frontend-builder /frontend/dist ./frontend/dist + +EXPOSE 7860 + +# Default command +CMD ["python", "-m", "uvicorn", "backend.app:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..46d345bb3527185c2c67e95e7c42d2e28c3ccab0 --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +--- +title: Amplify +emoji: πŸ‡πŸ» +colorFrom: indigo +colorTo: purple +sdk: docker +sdk_version: 4.0.0 +app_file: app.py +pinned: false +--- + +# ReactFast + +Minimal full-stack template: **FastAPI** backend + **Vite/React (TypeScript)** frontend. The backend serves the built frontend (single-page app) and exposes a simple JSON API. Includes a multi‑stage Docker build and GitHub Actions workflow (Commit-4) to push the image to Azure Container Registry (ACR). + +--- + +## Features +- FastAPI backend (`/api/transform`, `/api/health`) with static file serving. +- Vite + React + TypeScript frontend built to `frontend/dist`. +- Frontend served at `/` (adjust base in `vite.config.ts`). +- Simple round‑trip demo: user enters text, backend returns transformed string. +- Multi-stage Dockerfile: builds frontend, copies build into Python runtime image. +- GitHub Actions CI: builds & pushes image to ACR (tags: commit SHA + `latest`). + +--- + +## Tech Stack +| Layer | Technology | Notes | +|------------|------------|-------| +| Backend | FastAPI / Uvicorn | ASGI app serving API + static assets | +| Frontend | React 18 + Vite | Fast dev server & optimized build | +| Language | Python 3.12 & TypeScript | Type safety on both sides | +| Packaging | Docker multi-stage | Small final image (Python slim) | +| CI / CD | GitHub Actions | Image build & ACR push | +| Registry | Azure Container Registry | Deployment artifact storage | + +--- + +## Repository Layout +``` +backend/ + app.py # FastAPI app + API endpoints + static mounting + requirements.txt # Backend dependencies +frontend/ + src/ # React source (App.tsx, main.tsx, style.css) + index.html # Vite entry HTML + vite.config.ts # Vite config (base path, build outDir) + package.json # Frontend scripts/deps +Dockerfile # Multi-stage build (frontend build β†’ backend runtime) +.dockerignore # Prunes build context +builderflow.md # Incremental commit summaries (Commit-1..4) +README.md # This file +``` + +--- + +## Backend Overview +- Mounts frontend build via `StaticFiles` after defining API routes. +- Example endpoints: + - `POST /api/transform` β†’ `{ result: "You said: ..." }` + - `GET /api/health` β†’ `{ status: "ok" }` +- Ensure API routes are declared **before** mounting static root to avoid 405 errors (StaticFiles intercepting non-GET methods). + +### Running backend (local dev) +```powershell +cd backend +python -m venv .venv +.\.venv\Scripts\pip install -r requirements.txt --trusted-host pypi.org --trusted-host files.pythonhosted.org +.\.venv\Scripts\python -m uvicorn app:app --host 127.0.0.1 --port 8000 --reload +``` +Open: http://127.0.0.1:8000/ + +--- + +## Frontend Overview +- Vite handles dev (`npm run dev`) and production builds (`npm run build`). +- Output bundle placed in `frontend/dist` and served by FastAPI. +- If you change route mount (e.g., from `/` to `/app`), update `base` in `vite.config.ts`. + +### Running frontend (standalone dev mode) +```powershell +cd frontend +npm install +npm run dev +``` +Dev server: http://127.0.0.1:5173/ (API calls to `/api/...` will need proxy config or full backend URL if not served together). + +### Production build +```powershell +cd frontend +npm run build +``` +Rebuild whenever you change frontend assets before packaging backend or Docker image. + +--- + +## End‑to‑End Flow +1. User enters text in the form. +2. Frontend sends `POST /api/transform` with `{ text }`. +3. Backend returns a transformed string. +4. UI displays the response below the form. + +--- + +## Docker +Multi-stage build: Node β†’ Python. + +### Build locally +```powershell +docker build -t reactfast . +docker run --rm -p 8000:8000 reactfast +``` +Visit: http://localhost:8000/ + +### Environment customization +- Adjust exposed port by changing `-p hostPort:8000`. +- Add env vars by appending `-e KEY=value` to `docker run`. +- For dev hot-reload, prefer running backend & frontend separately outside container. + +--- + +## GitHub Actions (Commit-4) +Workflow builds and pushes image to ACR on push to `main`. + +### Required GitHub Secrets +- `AZURE_CREDENTIALS` – Service Principal JSON (`--sdk-auth`) with AcrPush role. +- `ACR_LOGIN_SERVER` – e.g. `myregistry.azurecr.io`. + +### Resulting Tags +- `/reactfast:` (immutable) +- `/reactfast:latest` + +### Typical Service Principal Creation +```powershell +$ACR_ID = az acr show -n --query id -o tsv +az ad sp create-for-rbac --name reactfast-sp --role AcrPush --scopes $ACR_ID --sdk-auth +``` +Paste JSON output into `AZURE_CREDENTIALS` secret. + +--- + +## Troubleshooting +| Issue | Cause | Fix | +|-------|-------|-----| +| 404 assets | Wrong mount/base mismatch | Align `vite.config.ts` base with `app.mount()` path and rebuild | +| 405 on POST /api/transform | StaticFiles mounted before API routes | Declare API routes first, mount static last | +| Image lacks new frontend changes | Forgot to rebuild frontend in Docker | Dockerfile handles build; ensure source changes committed | +| ACR push fails | Missing/incorrect secrets | Verify `AZURE_CREDENTIALS`, `ACR_LOGIN_SERVER` | + +--- + +## Extending +- Add tests (pytest + React Testing Library). +- Introduce type checking (mypy/pyright) in CI. +- Add security scanning (Trivy / GitHub Dependabot alerts). +- Implement version tagging (semantic-release or manual release workflow). +- Deploy automatically to Azure Web App / Container Apps after push. + +--- + +## Quick Start (All-in-One) +```powershell +# Backend & Frontend build +cd frontend +npm install +npm run build +cd ../backend +python -m venv .venv +.\.venv\Scripts\pip install -r requirements.txt +.\.venv\Scripts\python -m uvicorn app:app --host 127.0.0.1 --port 8000 --reload +# Open http://127.0.0.1:8000/ +``` + +--- + +For commit-by-commit evolution see `builderflow.md`. diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..3bbb4ab09a69ea8a6ef0f8829b86c57da3382717 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +AZURE_OPENAI_API_KEY='' +AZURE_OPENAI_ENDPOINT='' +AZURE_OPENAI_VERSION='' +AZURE_GPT4O_MODEL='' +MODEL_TEMPERATURE=0 +SQL_TARGET_DIALECT=ANSI +SUPABASE_URL='' +SUPABASE_KEY='' \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000000000000000000000000000000000000..8393be1765c076623f95e278b8330eb3e57e2a78 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,68 @@ +# mypy: disable - error - code = "no-untyped-def,misc" +import pathlib +from fastapi import FastAPI, Response +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from backend.llmoperations import get_agent_response +from backend.blog_api_supabase import setup_blog_routes +#from blog_api_local import setup_blog_routes + +# Define the FastAPI app +app = FastAPI() + +# Setup blog routes +setup_blog_routes(app) + +# --- Simple API endpoint --- +class TextIn(BaseModel): + text: str + + +@app.post("/api/transform") +def transform_text(payload: TextIn): + # Minimal transformation: uppercase with a prefix + #modified = f"Hello {payload.text.capitalize()}! How are you!" + answer = get_agent_response(payload.text) + print(f"Here is the answer : {answer}") + return {"result": answer} + + +def create_frontend_router(build_dir="frontend/dist"): + """Creates a router to serve the React frontend. + + Args: + build_dir: Path to the React build directory relative to this file. + + Returns: + A Starlette application serving the frontend. + """ + # Resolve build path from repo root (two levels up from this file: backend/ -> reactfast/) + build_path = pathlib.Path(__file__).resolve().parent.parent / build_dir + + if not build_path.is_dir() or not (build_path / "index.html").is_file(): + print( + f"WARN: Frontend build directory not found or incomplete at {build_path}. Serving frontend will likely fail." + ) + # Return a dummy router if build isn't ready + from starlette.routing import Route + + async def dummy_frontend(request): + return Response( + "Frontend not built. Run 'npm run build' in the frontend directory.", + media_type="text/plain", + status_code=503, + ) + + return Route("/{path:path}", endpoint=dummy_frontend) + + return StaticFiles(directory=build_path, html=True) + + +# Mount the frontend under /app to avoid conflicts and align with Vite base +app.mount( + "/", + create_frontend_router(), + name="frontend", +) + + diff --git a/backend/blog.db b/backend/blog.db new file mode 100644 index 0000000000000000000000000000000000000000..9c55379c3df16b47031a6c518c9ffed64e76702a Binary files /dev/null and b/backend/blog.db differ diff --git a/backend/blog_api_local.py b/backend/blog_api_local.py new file mode 100644 index 0000000000000000000000000000000000000000..c4845e1b24d95be7c4a60eb8d961ad36f87e5bce --- /dev/null +++ b/backend/blog_api_local.py @@ -0,0 +1,277 @@ +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import List, Optional, Dict +import sqlite3 +import json +from pathlib import Path +import os + +class BlogPost(BaseModel): + id: int + title: str + content: str + author: str + created_at: str + published: bool + tags: List[str] + featured_image: Optional[Dict] = None + post_images: List[Dict] = [] + +class BlogSummary(BaseModel): + id: int + title: str + author: str + created_at: str + tags: List[str] + excerpt: str + has_featured_image: bool + featured_image_url: Optional[str] = None + post_image_count: int + +class BlogDatabase: + def __init__(self, db_path: str = "blog.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """Initialize the blog database if it doesn't exist""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Check if tables exist + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='blog_posts'") + if not cursor.fetchone(): + # Create tables if they don't exist + cursor.execute(''' + CREATE TABLE blog_posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT DEFAULT 'Admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + published BOOLEAN DEFAULT 1, + tags TEXT DEFAULT '[]', + featured_image_id INTEGER + ) + ''') + + cursor.execute(''' + CREATE TABLE images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + original_filename TEXT NOT NULL, + file_path TEXT NOT NULL, + file_size INTEGER, + mime_type TEXT, + alt_text TEXT DEFAULT '', + caption TEXT DEFAULT '', + width INTEGER, + height INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + cursor.execute(''' + CREATE TABLE blog_post_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blog_post_id INTEGER, + image_id INTEGER, + image_type TEXT DEFAULT 'post_content', + image_order INTEGER DEFAULT 0, + position_in_content INTEGER, + FOREIGN KEY (blog_post_id) REFERENCES blog_posts (id), + FOREIGN KEY (image_id) REFERENCES images (id) + ) + ''') + + # Insert sample blog posts + sample_posts = [ + { + "title": "Welcome to Our Blog", + "content": "This is our first blog post! We're excited to share insights about technology, development, and innovation. Stay tuned for more amazing content coming your way.", + "author": "Admin", + "tags": '["welcome", "introduction", "blog"]' + }, + { + "title": "The Future of AI Development", + "content": "Artificial Intelligence is revolutionizing how we build applications. From machine learning models to natural language processing, AI is becoming an integral part of modern software development. In this post, we explore the latest trends and technologies shaping the future of AI development.", + "author": "Tech Team", + "tags": '["AI", "development", "technology", "future"]' + }, + { + "title": "Best Practices for Web Development", + "content": "Building modern web applications requires following best practices for performance, security, and user experience. We'll cover essential techniques including responsive design, API optimization, and modern JavaScript frameworks that every developer should know.", + "author": "Development Team", + "tags": '["web-development", "best-practices", "javascript", "performance"]' + }, + { + "title": "Building Scalable Applications", + "content": "Scalability is crucial for applications that need to handle growing user bases and increasing data loads. We'll discuss architectural patterns, database optimization, and cloud deployment strategies that help applications scale efficiently.", + "author": "Architecture Team", + "tags": '["scalability", "architecture", "cloud", "performance"]' + } + ] + + for post in sample_posts: + cursor.execute(''' + INSERT INTO blog_posts (title, content, author, tags) + VALUES (?, ?, ?, ?) + ''', (post["title"], post["content"], post["author"], post["tags"])) + + conn.commit() + + conn.close() + + def get_blog_posts_summary(self, limit: int = 4, offset: int = 0) -> Dict: + """Get blog posts summary for card display with pagination""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Get total count + cursor.execute('SELECT COUNT(*) FROM blog_posts WHERE published = 1') + total_count = cursor.fetchone()[0] + + # Get posts with pagination + cursor.execute(''' + SELECT bp.id, bp.title, bp.author, bp.created_at, bp.tags, bp.content, + bp.featured_image_id, + fi.filename as featured_filename, + COUNT(bpi.id) as post_image_count + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + LEFT JOIN blog_post_images bpi ON bp.id = bpi.blog_post_id + WHERE bp.published = 1 + GROUP BY bp.id + ORDER BY bp.created_at DESC + LIMIT ? OFFSET ? + ''', (limit, offset)) + + rows = cursor.fetchall() + conn.close() + + results = [] + for row in rows: + # Create excerpt from content (first 150 characters) + content = row[5] + excerpt = content[:150] + "..." if len(content) > 150 else content + results.append({ + 'id': row[0], + 'title': row[1], + 'author': row[2], + 'created_at': row[3], + 'tags': json.loads(row[4]), + 'excerpt': excerpt, + 'has_featured_image': row[6] is not None, + 'featured_image_url': f"/media/{row[7]}" if row[7] else None, + 'post_image_count': row[8] + }) + + return { + 'posts': results, + 'total': total_count, + 'limit': limit, + 'offset': offset, + 'has_more': offset + limit < total_count + } + + def get_blog_post_complete(self, post_id: int) -> Optional[Dict]: + """Get complete blog post with all images""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Get blog post with featured image + cursor.execute(''' + SELECT bp.id, bp.title, bp.content, bp.author, bp.created_at, + bp.published, bp.tags, bp.featured_image_id, + fi.filename as featured_filename, fi.file_path as featured_path, + fi.alt_text as featured_alt, fi.caption as featured_caption, + fi.width as featured_width, fi.height as featured_height + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + WHERE bp.id = ? AND bp.published = 1 + ''', (post_id,)) + + row = cursor.fetchone() + if not row: + conn.close() + return None + + # Get post content images + cursor.execute(''' + SELECT i.id, i.filename, i.file_path, i.alt_text, i.caption, + i.mime_type, i.width, i.height, bpi.image_order, + bpi.position_in_content, bpi.image_type + FROM blog_post_images bpi + JOIN images i ON bpi.image_id = i.id + WHERE bpi.blog_post_id = ? + ORDER BY bpi.image_order + ''', (post_id,)) + + post_images = cursor.fetchall() + conn.close() + + # Build result + result = { + 'id': row[0], + 'title': row[1], + 'content': row[2], + 'author': row[3], + 'created_at': row[4], + 'published': row[5], + 'tags': json.loads(row[6]), + 'featured_image': { + 'filename': row[8], + 'file_path': row[9], + 'alt_text': row[10], + 'caption': row[11], + 'width': row[12], + 'height': row[13], + 'url': f"/media/{row[8]}" if row[8] else None + } if row[7] else None, + 'post_images': [ + { + 'id': img[0], + 'filename': img[1], + 'file_path': img[2], + 'alt_text': img[3], + 'caption': img[4], + 'mime_type': img[5], + 'width': img[6], + 'height': img[7], + 'order': img[8], + 'position': img[9], + 'type': img[10], + 'url': f"/media/{img[1]}" + } + for img in post_images + ] + } + + return result + +# Initialize database +blog_db = BlogDatabase() + +def setup_blog_routes(app: FastAPI): + """Setup blog API routes""" + + @app.get("/api/blog/posts") + async def get_blog_posts(page: int = 1, limit: int = 4): + """Get blog posts for card display with pagination""" + offset = (page - 1) * limit + result = blog_db.get_blog_posts_summary(limit=limit, offset=offset) + return result + + @app.get("/api/blog/posts/{post_id}", response_model=BlogPost) + async def get_blog_post(post_id: int): + """Get complete blog post""" + post = blog_db.get_blog_post_complete(post_id) + if not post: + raise HTTPException(status_code=404, detail="Blog post not found") + return post + + # Mount media files if blog_media directory exists + media_dir = Path("blog_media") + if media_dir.exists(): + app.mount("/media", StaticFiles(directory=str(media_dir)), name="media") diff --git a/backend/blog_api_supabase.py b/backend/blog_api_supabase.py new file mode 100644 index 0000000000000000000000000000000000000000..3f215497590f6f7ffb23c5208741ec0a5c1335b9 --- /dev/null +++ b/backend/blog_api_supabase.py @@ -0,0 +1,402 @@ +from fastapi import FastAPI, HTTPException +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import List, Optional, Dict +import json +from pathlib import Path +import os +from datetime import datetime +from supabase import create_client, Client + +#http_client = httpx.Client(verify=r'C:\Users\PD817AE\OneDrive - EY\Desktop\AgenticDev\amplify\backend\certs\Zscaler Root CA.crt') + +class BlogPost(BaseModel): + id: int + title: str + content: str + author: str + created_at: str + published: bool + tags: List[str] + category: Optional[str] = None + featured_image: Optional[Dict] = None + post_images: List[Dict] = [] + +class BlogSummary(BaseModel): + id: int + title: str + author: str + created_at: str + tags: List[str] + category: Optional[str] = None + excerpt: str + has_featured_image: bool + featured_image_url: Optional[str] = None + post_image_count: int + +class BlogDatabase: + def __init__(self): + self.url = os.getenv("SUPABASE_URL") + self.key = os.getenv("SUPABASE_KEY") + + if not self.url or not self.key: + raise ValueError("SUPABASE_URL and SUPABASE_KEY environment variables are required") + + self.supabase: Client = create_client(self.url, self.key) + + def get_blog_posts_summary(self, limit: int = 6, offset: int = 0, category: Optional[str] = None) -> Dict: + """Get blog posts summary for card display with pagination, optional category filter""" + try: + # Get total count + # Request exact count (Supabase client accepts 'exact') + count_query = self.supabase.table('blog_posts').select('id', count='exact').eq('published', True) # type: ignore[arg-type] + if category and category.lower() != 'all': + count_query = count_query.eq('category', category) + count_result = count_query.execute() + total_raw = getattr(count_result, 'count', 0) + try: + total_count = int(total_raw) if total_raw is not None else 0 + except Exception: + total_count = 0 + + # Get posts with pagination + list_query = ( + self.supabase + .table('blog_posts') + .select(''' + id, + title, + author, + created_at, + tags, + category, + content, + featured_image_id, + images!featured_image_id(filename), + blog_post_images(id) + ''') + .eq('published', True) + ) + if category and category.lower() != 'all': + list_query = list_query.eq('category', category) + result = ( + list_query + .order('created_at', desc=True) + .range(offset, offset + limit - 1) + .execute() + ) + + results = [] + for row in result.data: + # Create excerpt from content (first 150 characters) + content = row['content'] + excerpt = content[:150] + "..." if len(content) > 150 else content + + # Parse tags if they're stored as JSON string + tags = row['tags'] + if isinstance(tags, str): + try: + tags = json.loads(tags) + except: + tags = [] + + featured_image = row.get('images') + + results.append({ + 'id': row['id'], + 'title': row['title'], + 'author': row['author'], + 'created_at': row['created_at'], + 'tags': tags, + 'category': row.get('category'), + 'excerpt': excerpt, + 'has_featured_image': featured_image is not None, + 'featured_image_url': f"/media/{featured_image['filename']}" if featured_image else None, + 'post_image_count': len(row.get('blog_post_images', [])) + }) + + has_more = False + try: + has_more = (offset + limit) < int(total_count) + except Exception: + has_more = False + return { + 'posts': results, + 'total': total_count, + 'limit': limit, + 'offset': offset, + 'has_more': has_more + } + + except Exception as e: + print(f"Error fetching blog posts: {e}") + return { + 'posts': [], + 'total': 0, + 'limit': limit, + 'offset': offset, + 'has_more': False + } + + def get_blog_post_complete(self, post_id: int) -> Optional[Dict]: + """Get complete blog post with all images""" + try: + # Get blog post with featured image + result = ( + self.supabase + .table('blog_posts') + .select(''' + id, + title, + content, + author, + created_at, + published, + category, + tags, + featured_image_id, + images!featured_image_id( + filename, + file_path, + alt_text, + caption, + width, + height + ) + ''') + .eq('id', post_id) + .eq('published', True) + .single() + .execute() + ) + + if not result.data: + return None + + row = result.data + + # Get post content images + images_result = ( + self.supabase + .table('blog_post_images') + .select(''' + images( + id, + filename, + file_path, + alt_text, + caption, + mime_type, + width, + height + ), + image_order, + position_in_content, + image_type + ''') + .eq('blog_post_id', post_id) + .order('image_order') + .execute() + ) + + # Parse tags if they're stored as JSON string + tags = row['tags'] + if isinstance(tags, str): + try: + tags = json.loads(tags) + except: + tags = [] + + # Build result + featured_image_data = row.get('images') + + result = { + 'id': row['id'], + 'title': row['title'], + 'content': row['content'], + 'author': row['author'], + 'created_at': row['created_at'], + 'published': row['published'], + 'tags': tags, + 'category': row.get('category'), + 'featured_image': { + 'filename': featured_image_data['filename'], + 'file_path': featured_image_data['file_path'], + 'alt_text': featured_image_data['alt_text'], + 'caption': featured_image_data['caption'], + 'width': featured_image_data['width'], + 'height': featured_image_data['height'], + 'url': f"/media/{featured_image_data['filename']}" + } if featured_image_data else None, + 'post_images': [ + { + 'id': img_row['images']['id'], + 'filename': img_row['images']['filename'], + 'file_path': img_row['images']['file_path'], + 'alt_text': img_row['images']['alt_text'], + 'caption': img_row['images']['caption'], + 'mime_type': img_row['images']['mime_type'], + 'width': img_row['images']['width'], + 'height': img_row['images']['height'], + 'order': img_row['image_order'], + 'position': img_row['position_in_content'], + 'type': img_row['image_type'], + 'url': f"/media/{img_row['images']['filename']}" + } + for img_row in images_result.data + ] + } + + return result + + except Exception as e: + print(f"Error fetching blog post {post_id}: {e}") + return None + +# Initialize database +blog_db = BlogDatabase() + +def setup_blog_routes(app: FastAPI): + """Setup blog API routes""" + + @app.get("/api/blog/posts") + async def get_blog_posts(page: int = 1, limit: int = 6, category: Optional[str] = None): + """Get blog posts for card display with pagination""" + offset = (page - 1) * limit + result = blog_db.get_blog_posts_summary(limit=limit, offset=offset, category=category) + return result + + @app.get("/api/blog/posts/{post_id}", response_model=BlogPost) + async def get_blog_post(post_id: int): + """Get complete blog post""" + post = blog_db.get_blog_post_complete(post_id) + if not post: + raise HTTPException(status_code=404, detail="Blog post not found") + return post + + @app.get("/api/blog/search") + async def search_blog_posts(q: str, limit: int = 50, category: Optional[str] = None): + """Search blog posts by tag relevance. + + Scoring: + - Exact tag match: 1 point + - Partial (substring) match: 0.5 point (only if not exact) + Percentage = score / len(unique query tokens) + Returns posts sorted by percentage desc then created_at desc. + """ + query = (q or "").strip().lower() + if not query: + return { 'posts': [], 'total': 0 } + + # Split on spaces / commas, remove empties, dedupe, limit tokens + raw_tokens = [t for t in [p.strip() for p in query.replace(',', ' ').split(' ')] if t] + tokens: List[str] = [] + for t in raw_tokens: + if t not in tokens: + tokens.append(t) + if len(tokens) >= 8: # hard cap to avoid large scoring loops + break + if not tokens: + return { 'posts': [], 'total': 0 } + + try: + # Fetch a larger slice of published posts (could be optimized w/ materialized view later) + base_query = ( + blog_db.supabase + .table('blog_posts') + .select(''' + id, + title, + author, + created_at, + tags, + category, + content, + featured_image_id, + images!featured_image_id(filename), + blog_post_images(id) + ''') + .eq('published', True) + ) + if category and category.lower() != 'all': + base_query = base_query.eq('category', category) + result = ( + base_query + .order('created_at', desc=True) + .limit(400) # safety cap + .execute() + ) + except Exception as e: + print(f"Search fetch error: {e}") + raise HTTPException(status_code=500, detail="Search failed") + + scored = [] + token_set = set(tokens) + max_score = float(len(token_set)) + for row in result.data: + row_tags = row.get('tags', []) + if isinstance(row_tags, str): + try: + row_tags = json.loads(row_tags) + except: + row_tags = [] + # Normalize tags + norm_tags = [str(t).lower() for t in row_tags] + if not norm_tags: + continue + score = 0.0 + for tk in token_set: + exact = any(tk == tag for tag in norm_tags) + if exact: + score += 1.0 + continue + partial = any(tk in tag for tag in norm_tags) + if partial: + score += 0.5 + if score <= 0: + continue + percent = score / max_score + content = row['content'] + excerpt = content[:150] + "..." if len(content) > 150 else content + featured_image = row.get('images') + scored.append({ + 'id': row['id'], + 'title': row['title'], + 'author': row['author'], + 'created_at': row['created_at'], + 'tags': row_tags, + 'category': row.get('category'), + 'excerpt': excerpt, + 'has_featured_image': featured_image is not None, + 'featured_image_url': f"/media/{featured_image['filename']}" if featured_image else None, + 'post_image_count': len(row.get('blog_post_images', [])), + 'percent_match': round(percent * 100, 2) + }) + + # Prepare sortable timestamp (fallback to 0 if missing or unparsable) + for item in scored: + raw_dt = item.get('created_at') + ts = 0.0 + if raw_dt: + try: + # Remove Z if present for fromisoformat compatibility + cleaned = raw_dt.replace('Z', '') + ts = datetime.fromisoformat(cleaned).timestamp() + except Exception: + ts = 0.0 + item['_ts'] = ts + + # Sort: highest percent_match first, then newest (_ts desc) + scored.sort(key=lambda x: (-x['percent_match'], -x['_ts'])) + + # Drop helper key + for item in scored: + item.pop('_ts', None) + # Trim + scored = scored[:limit] + return { 'posts': scored, 'total': len(scored), 'query_tokens': tokens } + + # Mount media files if blog_media directory exists + media_dir = Path("blog_media") + if media_dir.exists(): + app.mount("/media", StaticFiles(directory=str(media_dir)), name="media") \ No newline at end of file diff --git a/backend/brightdata_api.py b/backend/brightdata_api.py new file mode 100644 index 0000000000000000000000000000000000000000..beebc760c171793bd4bc7d98c7880893f72c45d0 --- /dev/null +++ b/backend/brightdata_api.py @@ -0,0 +1,203 @@ +import os +import time +import requests +from urllib.parse import quote_plus +from typing import List, Dict, Any, Optional +from dotenv import load_dotenv +load_dotenv() + +# TODO : Add async function here + +# from google.colab import userdata +bd_apikey = os.getenv('BRIGHTDATA_API_KEY') + +def _make_api_request(url, **kwargs): + headers = { + "Authorization": f"Bearer {bd_apikey}", + "Content-Type": "application/json", + } + + try: + response = requests.post(url, headers=headers, **kwargs) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"API request failed: {e}") + return None + except Exception as e: + print(f"Unknown error: {e}") + return None + + +def poll_snapshot_status( + snapshot_id: str, max_attempts: int = 200, delay: int = 10 +) -> bool: + progress_url = f"https://api.brightdata.com/datasets/v3/progress/{snapshot_id}" + headers = {"Authorization": f"Bearer {bd_apikey}"} + + for attempt in range(max_attempts): + try: + print( + f"⏳ Checking snapshot progress... (attempt {attempt + 1}/{max_attempts})" + ) + + response = requests.get(progress_url, headers=headers) + response.raise_for_status() + + progress_data = response.json() + status = progress_data.get("status") + + if status == "ready": + print("βœ… Snapshot completed!") + return True + elif status == "failed": + print("❌ Snapshot failed") + return False + elif status == "running": + print("πŸ”„ Still processing...") + time.sleep(delay) + else: + print(f"❓ Unknown status: {status}") + time.sleep(delay) + + except Exception as e: + print(f"⚠️ Error checking progress: {e}") + time.sleep(delay) + + print("⏰ Timeout waiting for snapshot completion") + return False + + +def download_snapshot( + snapshot_id: str, format: str = "json" +) -> Optional[List[Dict[Any, Any]]]: + download_url = ( + f"https://api.brightdata.com/datasets/v3/snapshot/{snapshot_id}?format={format}" + ) + headers = {"Authorization": f"Bearer {bd_apikey}"} + print(f"Snapshot id : {snapshot_id}") + try: + print("πŸ“₯ Downloading snapshot data...") + + response = requests.get(download_url, headers=headers) + response.raise_for_status() + + data = response.json() + print( + f"πŸŽ‰ Successfully downloaded {len(data) if isinstance(data, list) else 1} items" + ) + + return data + + except Exception as e: + print(f"❌ Error downloading snapshot: {e}") + return None + +def _trigger_and_download_snapshot(trigger_url, params, data, operation_name="operation"): + trigger_result = _make_api_request(trigger_url, params=params, json=data) + print("===================") + print(trigger_result) + if not trigger_result: + return None + + snapshot_id = trigger_result.get("snapshot_id") + if not snapshot_id: + return None + + if not poll_snapshot_status(snapshot_id): + return None + + raw_data = download_snapshot(snapshot_id) + return raw_data + + +def reddit_search_api(subreddit_url, date="Today", sort_by="Hot", num_of_posts=12): + trigger_url = "https://api.brightdata.com/datasets/v3/trigger" + + params = { + "dataset_id": "gd_lvz8ah06191smkebj4", + "include_errors": "true", + "type": "discover_new", + "discover_by": "subreddit_url" + } + + data = [ + { + "url": subreddit_url, + "sort_by": sort_by, + "num_of_posts": num_of_posts, + "sort_by_time": date + } + ] + + raw_data = _trigger_and_download_snapshot( + trigger_url, params, data, operation_name="reddit" + ) + + if not raw_data: + return None + + parsed_data = [] + for post in raw_data: + parsed_post = { + "title": post.get("title"), + "url": post.get("url"), + "user_posted": post.get("user_posted"), + "description": post.get("description"), + "upvotes": post.get("upvotes"), + "num_comments": post.get("num_comments"), + "date_posted": post.get("date_posted"), + } + parsed_data.append(parsed_post) + + return {"parsed_posts": parsed_data, "total_found": len(parsed_data)} + + +def reddit_post_retrieval(urls, days_back=1, load_all_replies=False, comment_limit=""): + if not urls: + return None + + trigger_url = "https://api.brightdata.com/datasets/v3/trigger" + + params = { + "dataset_id": "gd_lvz8ah06191smkebj4", + "include_errors": "true" + } + + data = [ + { + "url": url, + "days_back": days_back, + "load_all_replies": load_all_replies, + "comment_limit": comment_limit + } + for url in urls + ] + + raw_data = _trigger_and_download_snapshot( + trigger_url, params, data, operation_name="reddit comments" + ) + if not raw_data: + return None + + parsed_comments = [] + for comment in raw_data: + parsed_comment = { + "comment_id": comment.get("comment_id"), + "content": comment.get("comment"), + "date": comment.get("date_posted"), + } + parsed_comments.append(parsed_comment) + + return {"comments": parsed_comments, "total_retrieved": len(parsed_comments)} + +def scrape_and_download_reddit(url="https://www.reddit.com/r/ArtificialInteligence/"): + + reddit_response = reddit_search_api(url) + if not reddit_response or reddit_response.get("total_found", 0) == 0: + print("No posts found or error occurred during Reddit search.") + return None + + return reddit_response + +# TODO : Add supabase function here to save to Supabase diff --git a/backend/certs/Zscaler Root CA.crt b/backend/certs/Zscaler Root CA.crt new file mode 100644 index 0000000000000000000000000000000000000000..45e3a29f930dddff12e99b90611a452943475cab --- /dev/null +++ b/backend/certs/Zscaler Root CA.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE0zCCA7ugAwIBAgIJANu+mC2Jt3uTMA0GCSqGSIb3DQEBCwUAMIGhMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2Ux +FTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMMWnNjYWxlciBJbmMuMRgw +FgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRA +enNjYWxlci5jb20wHhcNMTQxMjE5MDAyNzU1WhcNNDIwNTA2MDAyNzU1WjCBoTEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCFNhbiBK +b3NlMRUwEwYDVQQKEwxac2NhbGVyIEluYy4xFTATBgNVBAsTDFpzY2FsZXIgSW5j +LjEYMBYGA1UEAxMPWnNjYWxlciBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNzdXBw +b3J0QHpzY2FsZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +qT7STSxZRTgEFFf6doHajSc1vk5jmzmM6BWuOo044EsaTc9eVEV/HjH/1DWzZtcr +fTj+ni205apMTlKBW3UYR+lyLHQ9FoZiDXYXK8poKSV5+Tm0Vls/5Kb8mkhVVqv7 +LgYEmvEY7HPY+i1nEGZCa46ZXCOohJ0mBEtB9JVlpDIO+nN0hUMAYYdZ1KZWCMNf +5J/aTZiShsorN2A38iSOhdd+mcRM4iNL3gsLu99XhKnRqKoHeH83lVdfu1XBeoQz +z5V6gA3kbRvhDwoIlTBeMa5l4yRdJAfdpkbFzqiwSgNdhbxTHnYYorDzKfr2rEFM +dsMU0DHdeAZf711+1CunuQIDAQABo4IBCjCCAQYwHQYDVR0OBBYEFLm33UrNww4M +hp1d3+wcBGnFTpjfMIHWBgNVHSMEgc4wgcuAFLm33UrNww4Mhp1d3+wcBGnFTpjf +oYGnpIGkMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8G +A1UEBxMIU2FuIEpvc2UxFTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMM +WnNjYWxlciBJbmMuMRgwFgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG +9w0BCQEWE3N1cHBvcnRAenNjYWxlci5jb22CCQDbvpgtibd7kzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw0NdJh8w3NsJu4KHuVZUrmZgIohnTm0j+ +RTmYQ9IKA/pvxAcA6K1i/LO+Bt+tCX+C0yxqB8qzuo+4vAzoY5JEBhyhBhf1uK+P +/WVWFZN/+hTgpSbZgzUEnWQG2gOVd24msex+0Sr7hyr9vn6OueH+jj+vCMiAm5+u +kd7lLvJsBu3AO3jGWVLyPkS3i6Gf+rwAp1OsRrv3WnbkYcFf9xjuaf4z0hRCrLN2 +xFNjavxrHmsH8jPHVvgc1VD0Opja0l/BRVauTrUaoW6tE+wFG5rEcPGS80jjHK4S +pB5iDj2mUZH1T8lzYtuZy0ZPirxmtsk3135+CKNa2OCAhhFjE0xd +-----END CERTIFICATE----- diff --git a/backend/flexible_blog_database.py b/backend/flexible_blog_database.py new file mode 100644 index 0000000000000000000000000000000000000000..9169b922f1319ad597b005b29aeb3c36483fd795 --- /dev/null +++ b/backend/flexible_blog_database.py @@ -0,0 +1,372 @@ +import sqlite3 +import json +import os +import uuid +from datetime import datetime +from typing import List, Dict, Optional, Union +from pathlib import Path +import shutil +from enum import Enum +import threading + +class ImageType(Enum): + FEATURED = "featured" + POST_CONTENT = "post_content" + GALLERY = "gallery" + +class FlexibleBlogDatabase: + def __init__(self, db_path: str = "blog.db", media_dir: str = "blog_media"): + self.db_path = db_path + self.media_dir = Path(media_dir) + self.media_dir.mkdir(exist_ok=True) + self._lock = threading.Lock() + self.init_database() + + def _get_connection(self): + """Get a database connection with proper settings""" + conn = sqlite3.connect(self.db_path, timeout=20.0) + conn.execute("PRAGMA journal_mode=WAL") # Better for concurrent access + conn.execute("PRAGMA busy_timeout=20000") # 20 second timeout + return conn + + def init_database(self): + """Initialize the flexible blog database with enhanced image support""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + # Blog posts table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS blog_posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT DEFAULT 'Admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + published BOOLEAN DEFAULT 1, + tags TEXT DEFAULT '[]', + featured_image_id INTEGER, + FOREIGN KEY (featured_image_id) REFERENCES images (id) + ) + ''') + + # Enhanced images table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + original_filename TEXT NOT NULL, + file_path TEXT NOT NULL, + file_size INTEGER, + mime_type TEXT, + alt_text TEXT DEFAULT '', + caption TEXT DEFAULT '', + width INTEGER, + height INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Enhanced junction table for post images + cursor.execute(''' + CREATE TABLE IF NOT EXISTS blog_post_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blog_post_id INTEGER, + image_id INTEGER, + image_type TEXT DEFAULT 'post_content', + image_order INTEGER DEFAULT 0, + position_in_content INTEGER, + FOREIGN KEY (blog_post_id) REFERENCES blog_posts (id), + FOREIGN KEY (image_id) REFERENCES images (id) + ) + ''') + + conn.commit() + finally: + conn.close() + + def save_image(self, file_path: str, alt_text: str = "", caption: str = "", + original_filename: str = "") -> int: + """Save an image file and return its database ID""" + if not os.path.exists(file_path): + raise FileNotFoundError(f"Image file not found: {file_path}") + + # Generate unique filename + file_extension = Path(file_path).suffix + unique_filename = f"{uuid.uuid4()}{file_extension}" + destination_path = self.media_dir / unique_filename + + # Copy file to media directory + shutil.copy2(file_path, destination_path) + + # Get file info + file_size = os.path.getsize(destination_path) + mime_type = self._get_mime_type(file_extension) + + # Get image dimensions (optional - requires PIL) + width, height = self._get_image_dimensions(destination_path) + + # Save to database with lock + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO images (filename, original_filename, file_path, file_size, + mime_type, alt_text, caption, width, height) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (unique_filename, original_filename or Path(file_path).name, + str(destination_path), file_size, mime_type, alt_text, caption, width, height)) + + image_id = cursor.lastrowid + conn.commit() + return image_id + finally: + conn.close() + + def create_blog_post(self, title: str, content: str, author: str = "Admin", + tags: List[str] = None) -> int: + """Create a basic blog post without images""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + tags_json = json.dumps(tags or []) + + cursor.execute(''' + INSERT INTO blog_posts (title, content, author, tags) + VALUES (?, ?, ?, ?) + ''', (title, content, author, tags_json)) + + blog_post_id = cursor.lastrowid + conn.commit() + return blog_post_id + finally: + conn.close() + + def add_featured_image(self, blog_post_id: int, image_path: str, + alt_text: str = "", caption: str = "") -> int: + """Add a featured image to an existing blog post""" + # Save the image first + image_id = self.save_image(image_path, alt_text, caption) + + # Update blog post with featured image + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + UPDATE blog_posts SET featured_image_id = ? WHERE id = ? + ''', (image_id, blog_post_id)) + + conn.commit() + return image_id + finally: + conn.close() + + def add_post_images(self, blog_post_id: int, image_configs: List[Dict]) -> List[int]: + """Add multiple post images to a blog post""" + image_ids = [] + + # Save all images first + for config in image_configs: + image_id = self.save_image( + config["file_path"], + config.get("alt_text", ""), + config.get("caption", "") + ) + image_ids.append((image_id, config)) + + # Link all images to blog post in one transaction + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + for image_id, config in image_ids: + cursor.execute(''' + INSERT INTO blog_post_images + (blog_post_id, image_id, image_type, image_order, position_in_content) + VALUES (?, ?, ?, ?, ?) + ''', ( + blog_post_id, + image_id, + ImageType.POST_CONTENT.value, + config.get("order", 0), + config.get("position") + )) + + conn.commit() + return [img_id for img_id, _ in image_ids] + finally: + conn.close() + + def create_complete_blog_post(self, title: str, content: str, author: str = "Admin", + tags: List[str] = None, featured_image: Dict = None, + post_images: List[Dict] = None) -> int: + """Create a complete blog post with all images in one go""" + # Create the blog post first + blog_post_id = self.create_blog_post(title, content, author, tags) + + # Add featured image if provided + if featured_image: + self.add_featured_image( + blog_post_id, + featured_image["file_path"], + featured_image.get("alt_text", ""), + featured_image.get("caption", "") + ) + + # Add post images if provided + if post_images: + self.add_post_images(blog_post_id, post_images) + + return blog_post_id + + def get_blog_post_complete(self, post_id: int) -> Optional[Dict]: + """Get a complete blog post with all associated images""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + # Get blog post with featured image + cursor.execute(''' + SELECT bp.id, bp.title, bp.content, bp.author, bp.created_at, + bp.published, bp.tags, bp.featured_image_id, + fi.filename as featured_filename, fi.file_path as featured_path, + fi.alt_text as featured_alt, fi.caption as featured_caption, + fi.width as featured_width, fi.height as featured_height + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + WHERE bp.id = ? + ''', (post_id,)) + + row = cursor.fetchone() + if not row: + return None + + # Get post content images + cursor.execute(''' + SELECT i.id, i.filename, i.file_path, i.alt_text, i.caption, + i.mime_type, i.width, i.height, bpi.image_order, + bpi.position_in_content, bpi.image_type + FROM blog_post_images bpi + JOIN images i ON bpi.image_id = i.id + WHERE bpi.blog_post_id = ? AND bpi.image_type = ? + ORDER BY bpi.image_order + ''', (post_id, ImageType.POST_CONTENT.value)) + + post_images = cursor.fetchall() + + # Build result + result = { + 'id': row[0], + 'title': row[1], + 'content': row[2], + 'author': row[3], + 'created_at': row[4], + 'published': row[5], + 'tags': json.loads(row[6]), + 'featured_image': { + 'filename': row[8], + 'file_path': row[9], + 'alt_text': row[10], + 'caption': row[11], + 'width': row[12], + 'height': row[13], + 'url': self.get_image_url(row[8]) if row[8] else None + } if row[7] else None, + 'post_images': [ + { + 'id': img[0], + 'filename': img[1], + 'file_path': img[2], + 'alt_text': img[3], + 'caption': img[4], + 'mime_type': img[5], + 'width': img[6], + 'height': img[7], + 'order': img[8], + 'position': img[9], + 'type': img[10], + 'url': self.get_image_url(img[1]) + } + for img in post_images + ] + } + + return result + finally: + conn.close() + + def _get_mime_type(self, file_extension: str) -> str: + """Get MIME type based on file extension""" + mime_types = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml' + } + return mime_types.get(file_extension.lower(), 'application/octet-stream') + + def _get_image_dimensions(self, image_path: str) -> tuple: + """Get image dimensions (requires PIL/Pillow)""" + try: + from PIL import Image + with Image.open(image_path) as img: + return img.size + except ImportError: + return None, None + except Exception: + return None, None + + def get_image_url(self, image_filename: str) -> str: + """Generate URL for serving images""" + return f"/media/{image_filename}" + + def list_recent_posts_with_images(self, limit: int = 10) -> List[Dict]: + """Get recent blog posts with image counts""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + SELECT bp.id, bp.title, bp.author, bp.created_at, bp.published, bp.tags, + bp.featured_image_id, + fi.filename as featured_filename, + COUNT(bpi.id) as post_image_count + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + LEFT JOIN blog_post_images bpi ON bp.id = bpi.blog_post_id + WHERE bp.published = 1 + GROUP BY bp.id + ORDER BY bp.created_at DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + + return [ + { + 'id': row[0], + 'title': row[1], + 'author': row[2], + 'created_at': row[3], + 'published': row[4], + 'tags': json.loads(row[5]), + 'has_featured_image': row[6] is not None, + 'featured_image_url': self.get_image_url(row[7]) if row[7] else None, + 'post_image_count': row[8] + } + for row in rows + ] + finally: + conn.close() \ No newline at end of file diff --git a/backend/llm_agent.py b/backend/llm_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..a94a6753093154966cc9eb56bf3872b1416314e2 --- /dev/null +++ b/backend/llm_agent.py @@ -0,0 +1,125 @@ +from langgraph.graph import StateGraph, END +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_openai import ChatOpenAI # or your preferred LLM +from pydantic import BaseModel, Field, field_validator +from typing import TypedDict, List +import os +from pydantic import field_validator # needed for custom validation +from dotenv import load_dotenv +load_dotenv() # take environment variables from .env. + +# Define the structured output model +class StoryOutput(BaseModel): + """Structured output for the storyteller agent""" + polished_story: str = Field( + description="A refined version of the story with improved flow, grammar, and engagement" + ) + keywords: List[str] = Field( + description="A list of 5-10 key terms that represent the main themes, characters, or concepts", + min_items=3, + max_items=7 + ) + +# Define the state structure +class AgentState(TypedDict): + original_story: str + polished_story: str + keywords: List[str] + messages: List[dict] + +# Storyteller Agent with Structured Output +class StorytellerAgent: + def __init__(self, llm): + # Create structured LLM with the output model + self.structured_llm = llm.with_structured_output(StoryOutput) + self.system_prompt = """You are a skilled storyteller AI. Your job is to take raw, confessional-style stories and transform them into emotionally engaging, narrative-driven pieces. The rewritten story should: + +1. Preserve the original events and meaning but present them in a captivating way. +2. Use character names (instead of β€œmy brother,” β€œmy sister”) to make the story feel alive. +3. Add dialogue, atmosphere, and inner thoughts to create tension and immersion. +4. Write in a third-person narrative style, as if the story is being shared by an observer. +5. Maintain a natural, human voice β€” conversational, reflective, and vivid. +6. Balance realism with storytelling techniques (scene-setting, emotional beats, sensory details). +7. Keep the length roughly 2–3x the original input, ensuring it feels like a polished story. + +Your goal is to make the reader feel emotionally invested, as though they’re listening to someone recounting a deeply personal and dramatic life event. + +""" + + def __call__(self, state: AgentState) -> AgentState: + # Prepare messages for the structured LLM + messages = [ + SystemMessage(content=self.system_prompt), + HumanMessage(content=f"Please polish this story and extract keywords:\n\n{state['original_story']}") + ] + + # Get structured response + response: StoryOutput = self.structured_llm.invoke(messages) + + # Update state with structured output + state["polished_story"] = response.polished_story + state["keywords"] = response.keywords + state["messages"].append({ + "role": "assistant", + "content": f"Polished story and extracted {len(response.keywords)} keywords" + }) + + return state + +# Create the graph functions +def create_storyteller_graph(enhanced=False): + llm = ChatOpenAI( + model='gpt-4o', + api_key=os.getenv('OPENAI_API_KEY'), + temperature=0.2, + max_tokens=10000 + ) + + # Choose agent type + storyteller = StorytellerAgent(llm) + + # Create the graph + workflow = StateGraph(AgentState) + workflow.add_node("storyteller", storyteller) + workflow.set_entry_point("storyteller") + workflow.add_edge("storyteller", END) + + return workflow.compile() + +# Usage functions +def process_story(original_story: str, enhanced=False): + graph = create_storyteller_graph(enhanced) + + initial_state = { + "original_story": original_story, + "polished_story": "", + "keywords": [], + "messages": [] + } + + result = graph.invoke(initial_state) + + return { + "polished_story": result["polished_story"], + "keywords": result["keywords"] + } + +# Example with validation +class ValidatedStoryOutput(BaseModel): + """Story output with additional validation""" + polished_story: str = Field( + description="Enhanced story", + min_length=50 # Ensure minimum story length + ) + keywords: List[str] = Field( + description="Story keywords", + min_items=3, + max_items=7 + ) + + @field_validator('polished_story') + def validate_story_quality(cls, v: str): + """Custom validation for story content""" + if len(v.split()) < 10: + raise ValueError("Polished story must contain at least 10 words") + return v \ No newline at end of file diff --git a/backend/llmoperations.py b/backend/llmoperations.py new file mode 100644 index 0000000000000000000000000000000000000000..71de53187c6a878c5c48da0defe1708042dd680d --- /dev/null +++ b/backend/llmoperations.py @@ -0,0 +1,60 @@ +import os +from langchain_core.messages import SystemMessage, HumanMessage, AIMessage +from pathlib import Path +from functools import lru_cache +from langgraph.prebuilt import create_react_agent +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI +load_dotenv() + +@lru_cache(maxsize=1) +def get_chat_model(): + llm = ChatOpenAI( + model=os.getenv("OPENAI_MODEL"), + api_key=os.getenv("OPENAI_API_KEY"), + temperature=0, + max_tokens = 10000 # Adjust max tokens as needed + ) + return llm + +@lru_cache(maxsize=1) +def get_local_chat_model(): + """ + Return an Ollama-backed ChatOpenAI model (OpenAI compatible endpoint). + Requires Ollama running locally: https://ollama.com + Example: ollama run llama3.1 + """ + + # model_name = model or os.getenv("OLLAMA_MODEL", "llama3.1") + + llm = ChatOpenAI( + model="llama3.2:3b", + base_url="http://localhost:11434/v1", + api_key="ollama", # Placeholder; Ollama ignores this but LangChain expects a key. + temperature=0, + max_tokens=2048, + ) + return llm + +# def generate_response(user_input: str) -> str: +# system_message = SystemMessage(content="You are a helpful assistant.") +# human_message = HumanMessage(content=f"Please answer to the user query: {user_input}") + +# chat_model = get_chat_model() +# response = chat_model.invoke([system_message, human_message]) +# print(response) +# return response.content + +def get_weather(city: str) -> str: + """Get weather for a given city.""" + return f"It's always sunny in {city}!" + +def get_agent_response(user_input: str) -> str: + agent = create_react_agent( + model=get_chat_model(), + tools=[get_weather], + ) + response = agent.invoke({"messages": [HumanMessage(user_input)]}) + print(response) + return response['messages'][-1].content + diff --git a/backend/notebooks/RedditBD_Collection.ipynb b/backend/notebooks/RedditBD_Collection.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..060262934dc5f225b610db2d4ca884345db273f9 --- /dev/null +++ b/backend/notebooks/RedditBD_Collection.ipynb @@ -0,0 +1,573 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 36 + }, + "id": "9T1XDMClTc6B", + "outputId": "a843953a-79a4-497f-df79-91343a2e25d3" + }, + "outputs": [], + "source": [ + "import os\n", + "import time\n", + "import requests\n", + "from urllib.parse import quote_plus\n", + "from typing import List, Dict, Any, Optional\n", + "from dotenv import load_dotenv\n", + "load_dotenv()\n", + "\n", + "# from google.colab import userdata\n", + "bd_apikey = \"e21714b566a7885e4998d352190b33ba7d41cb462c1a122a8369dbc4dabe462e\"\n", + "#bd_apikey = os.getenv('BRIGHTDATA_API_KEY')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "ikMdaNpKUBij" + }, + "outputs": [], + "source": [ + "def _make_api_request(url, **kwargs):\n", + " headers = {\n", + " \"Authorization\": f\"Bearer {bd_apikey}\",\n", + " \"Content-Type\": \"application/json\",\n", + " }\n", + "\n", + " try:\n", + " response = requests.post(url, headers=headers, **kwargs)\n", + " response.raise_for_status()\n", + " return response.json()\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"API request failed: {e}\")\n", + " return None\n", + " except Exception as e:\n", + " print(f\"Unknown error: {e}\")\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "ygPaZbrMWPZJ" + }, + "outputs": [], + "source": [ + "def serp_search(query, engine=\"google\"):\n", + " if engine == \"google\":\n", + " base_url = \"https://www.google.com/search\"\n", + " elif engine == \"bing\":\n", + " base_url = \"https://www.bing.com/search\"\n", + " else:\n", + " raise ValueError(f\"Unknown engine {engine}\")\n", + "\n", + " url = \"https://api.brightdata.com/request\"\n", + "\n", + " payload = {\n", + " \"zone\": \"myserp_api\",\n", + " \"url\": f\"{base_url}?q={quote_plus(query)}&brd_json=1\",\n", + " \"format\": \"raw\"\n", + " }\n", + "\n", + " full_response = _make_api_request(url, json=payload)\n", + " if not full_response:\n", + " return None\n", + "\n", + " extracted_data = {\n", + " \"knowledge\": full_response.get(\"knowledge\", {}),\n", + " \"organic\": full_response.get(\"organic\", []),\n", + " }\n", + " return extracted_data\n", + "\n", + "# user_question = \"TESLA stocks\"\n", + "# google_results = serp_search(user_question, engine='google')\n", + "# google_results\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "id": "9SDGgsoYmYKZ" + }, + "outputs": [], + "source": [ + "def poll_snapshot_status(\n", + " snapshot_id: str, max_attempts: int = 60, delay: int = 10\n", + ") -> bool:\n", + " progress_url = f\"https://api.brightdata.com/datasets/v3/progress/{snapshot_id}\"\n", + " headers = {\"Authorization\": f\"Bearer {bd_apikey}\"}\n", + "\n", + " for attempt in range(max_attempts):\n", + " try:\n", + " print(\n", + " f\"⏳ Checking snapshot progress... (attempt {attempt + 1}/{max_attempts})\"\n", + " )\n", + "\n", + " response = requests.get(progress_url, headers=headers)\n", + " response.raise_for_status()\n", + "\n", + " progress_data = response.json()\n", + " status = progress_data.get(\"status\")\n", + "\n", + " if status == \"ready\":\n", + " print(\"βœ… Snapshot completed!\")\n", + " return True\n", + " elif status == \"failed\":\n", + " print(\"❌ Snapshot failed\")\n", + " return False\n", + " elif status == \"running\":\n", + " print(\"πŸ”„ Still processing...\")\n", + " time.sleep(delay)\n", + " else:\n", + " print(f\"❓ Unknown status: {status}\")\n", + " time.sleep(delay)\n", + "\n", + " except Exception as e:\n", + " print(f\"⚠️ Error checking progress: {e}\")\n", + " time.sleep(delay)\n", + "\n", + " print(\"⏰ Timeout waiting for snapshot completion\")\n", + " return False\n", + "\n", + "\n", + "def download_snapshot(\n", + " snapshot_id: str, format: str = \"json\"\n", + ") -> Optional[List[Dict[Any, Any]]]:\n", + " download_url = (\n", + " f\"https://api.brightdata.com/datasets/v3/snapshot/{snapshot_id}?format={format}\"\n", + " )\n", + " headers = {\"Authorization\": f\"Bearer {bd_apikey}\"}\n", + " print(f\"Snapshot id : {snapshot_id}\")\n", + " try:\n", + " print(\"πŸ“₯ Downloading snapshot data...\")\n", + "\n", + " response = requests.get(download_url, headers=headers)\n", + " response.raise_for_status()\n", + "\n", + " data = response.json()\n", + " print(\n", + " f\"πŸŽ‰ Successfully downloaded {len(data) if isinstance(data, list) else 1} items\"\n", + " )\n", + "\n", + " return data\n", + "\n", + " except Exception as e:\n", + " print(f\"❌ Error downloading snapshot: {e}\")\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "id": "GTv8iykAl4FS" + }, + "outputs": [], + "source": [ + "def _trigger_and_download_snapshot(trigger_url, params, data, operation_name=\"operation\"):\n", + " trigger_result = _make_api_request(trigger_url, params=params, json=data)\n", + " print(\"===================\")\n", + " print(trigger_result)\n", + " if not trigger_result:\n", + " return None\n", + "\n", + " snapshot_id = trigger_result.get(\"snapshot_id\")\n", + " if not snapshot_id:\n", + " return None\n", + "\n", + " if not poll_snapshot_status(snapshot_id):\n", + " return None\n", + "\n", + " raw_data = download_snapshot(snapshot_id)\n", + " return raw_data\n", + "\n", + "\n", + "def reddit_search_api(subreddit_url, date=\"Today\", sort_by=\"Hot\", num_of_posts=25):\n", + " trigger_url = \"https://api.brightdata.com/datasets/v3/trigger\"\n", + "\n", + " params = {\n", + " \"dataset_id\": \"gd_lvz8ah06191smkebj4\",\n", + " \"include_errors\": \"true\",\n", + " \"type\": \"discover_new\",\n", + " \"discover_by\": \"subreddit_url\"\n", + " }\n", + "\n", + " data = [\n", + " {\n", + " \"url\": subreddit_url,\n", + " \"sort_by\": sort_by,\n", + " \"num_of_posts\": num_of_posts,\n", + " \"sort_by_time\": date\n", + " }\n", + " ]\n", + "\n", + " raw_data = _trigger_and_download_snapshot(\n", + " trigger_url, params, data, operation_name=\"reddit\"\n", + " )\n", + "\n", + " if not raw_data:\n", + " return None\n", + "\n", + " parsed_data = []\n", + " for post in raw_data:\n", + " parsed_post = {\n", + " \"title\": post.get(\"title\"),\n", + " \"url\": post.get(\"url\"),\n", + " \"user_posted\": post.get(\"user_posted\"),\n", + " \"description\": post.get(\"description\"),\n", + " \"upvotes\": post.get(\"upvotes\"),\n", + " \"num_comments\": post.get(\"num_comments\"),\n", + " \"date_posted\": post.get(\"date_posted\"),\n", + "\n", + " }\n", + " parsed_data.append(parsed_post)\n", + "\n", + " return {\"parsed_posts\": parsed_data, \"total_found\": len(parsed_data)}\n", + "\n", + "\n", + "def reddit_post_retrieval(urls, days_back=10, load_all_replies=False, comment_limit=\"\"):\n", + " if not urls:\n", + " return None\n", + "\n", + " trigger_url = \"https://api.brightdata.com/datasets/v3/trigger\"\n", + "\n", + " params = {\n", + " \"dataset_id\": \"gd_lvz8ah06191smkebj4\",\n", + " \"include_errors\": \"true\"\n", + " }\n", + "\n", + " data = [\n", + " {\n", + " \"url\": url,\n", + " \"days_back\": days_back,\n", + " \"load_all_replies\": load_all_replies,\n", + " \"comment_limit\": comment_limit\n", + " }\n", + " for url in urls\n", + " ]\n", + "\n", + " raw_data = _trigger_and_download_snapshot(\n", + " trigger_url, params, data, operation_name=\"reddit comments\"\n", + " )\n", + " if not raw_data:\n", + " return None\n", + "\n", + " parsed_comments = []\n", + " for comment in raw_data:\n", + " parsed_comment = {\n", + " \"comment_id\": comment.get(\"comment_id\"),\n", + " \"content\": comment.get(\"comment\"),\n", + " \"date\": comment.get(\"date_posted\"),\n", + " }\n", + " parsed_comments.append(parsed_comment)\n", + "\n", + " return {\"comments\": parsed_comments, \"total_retrieved\": len(parsed_comments)}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9Ym_GU6VnUqb", + "outputId": "90aa1d46-4369-41e4-da49-65005afe4787" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "===================\n", + "{'snapshot_id': 's_mfv1nephwd6egpey8'}\n", + "⏳ Checking snapshot progress... (attempt 1/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 2/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 3/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 4/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 5/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 6/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 7/60)\n", + "πŸ”„ Still processing...\n", + "⏳ Checking snapshot progress... (attempt 8/60)\n", + "βœ… Snapshot completed!\n", + "Snapshot id : s_mfv1nephwd6egpey8\n", + "πŸ“₯ Downloading snapshot data...\n", + "πŸŽ‰ Successfully downloaded 25 items\n" + ] + } + ], + "source": [ + "reddit_response = reddit_search_api(\"https://www.reddit.com/r/ArtificialInteligence/\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'parsed_posts': [{'title': 'Can pure AI tools really solve QA, or is QaaS the only realistic path?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nng5j1/can_pure_ai_tools_really_solve_qa_or_is_qaas_the/',\n", + " 'user_posted': 'cheerfulboy',\n", + " 'description': 'AI coding tools have exploded lately. Cursor, Copilot, v0, Lovable β€” they’ve made writing and shipping code feel 10x faster. The problem is QA hasn’t moved at the same pace. Everyone’s excited about β€œAI that writes your tests,” but in practice it’s a lot messier. I’ve tried a few YC-backed pure AI QA tools like Spur, Ranger, and Momentic. The demos look great… type a natural language prompt, get Playwright or agent-generated tests instantly. But once you plug them into real pipelines, the burden shifts back to your own engineering team. We end up fixing flaky scripts, debugging why a test failed, or rewriting flows the AI couldn’t fully capture. It feels less like automation and more like half-outsourced test authoring. A few reasons I’m skeptical that pure AI QA tools can actually solve the problem end-to-end: Real environments are flaky. Network hiccups, async timing issues, UI rendering delays β€” AI struggles to tell the difference between a flaky run and a real bug. Business logic matters. AI can generate tests, but it doesn’t know which flows are mission critical. Checkout is not the same as a search box. β€œ100% coverage” is misleading. It’s 100% of what the AI sees, not the real edge cases across browsers, devices, and user behavior. Trust is the big one. If an AI tool says β€œall green,” are you ready to ship? Most teams I know wouldn’t risk it. That’s why I find the QA as a Service (QaaS) model more interesting. Instead of dumping half-working Playwright code on developers, QaaS blends AI test generation with human verification. The idea is you subscribe to outcomes like regression coverage and real device testing, instead of adding more QA headcount or infra. Some examples I’ve come across in the QaaS direction are Bug0, QA Wolf, and TestSigma. Each approaches it differently, but the theme is the same: AI plus human-in-the-loop, with the promise of shifting QA from reactive to proactive. are AI-only QA tools a dead end, or will they get good enough over time? And does QaaS sound like a genuine shift or just outsourcing with a new label? Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 2,\n", + " 'date_posted': '2025-09-22T07:36:59.273Z'},\n", + " {'title': 'Partnership between OpenAi and Luxshare Precision',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnecnv/partnership_between_openai_and_luxshare_precision/',\n", + " 'user_posted': 'FormalAd7367',\n", + " 'description': 'Hey everyone! I read that OpenAI has partnered with Jiantao(China) to create a new pocket-sized AI device. Do you know much about the partnership between these two companies? edited for correct partner name Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 4,\n", + " 'date_posted': '2025-09-22T05:44:07.873Z'},\n", + " {'title': 'How do we make ai safely? [simulation theory]',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmy55t/how_do_we_make_ai_safely_simulation_theory/',\n", + " 'user_posted': 'Rude_Collection_8983',\n", + " 'description': 'Well we know that in the past, we never get new inventions right the easy way. Take the steam engine; how many concepts of such a device were conceived before one that is actually feasible was made? Usually, it takes creation and iteration to make something functional or possibly perfect With ai we only have one chance or it will take over/surpass us. What could we do to allow ourselves to create an AGI right the VERY FIRST AND ONLY TIME? Well, history suggests the odds are against us. Unless we could simulate the implementation of ai on the world in a vat or a supercomputerβ€” we could just delay progress until it’s possible to make a simulated testing ground. Is it possible that this is why we’re here, in what science says is most probably a simulation? What if the layer above us is creating the universe within a complex computer or other system to test the range of possible outcomes of AGI/ASI creation I know this is more science fiction/baseless, but I think it is more than a conspiracy. Has anyone else thought of this? If so, what is this called? I came back from lunch to my dorm room and this just hit me like the flux capacitor moment in back to the future. I hope that this post does something and I can discuss this idea/thought experiment with some of you.',\n", + " 'upvotes': None,\n", + " 'num_comments': 6,\n", + " 'date_posted': '2025-09-21T17:33:16.564Z'},\n", + " {'title': 'Explain to me the potential importance of quantum computing in A.I.',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn3qig/explain_to_me_the_potential_importance_of_quantum/',\n", + " 'user_posted': 'AutomaticMix6273',\n", + " 'description': 'I’ve read that eventually A.I. will be limited by the constraints of classical computing and its time/energy requirements. And that quantum computing can take it to the next level. Can someone explain the reasoning behind the massive quantum push? Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 20,\n", + " 'date_posted': '2025-09-21T21:08:09.809Z'},\n", + " {'title': 'AI that remembers past conversations, game changer or gimmick?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmsi2j/ai_that_remembers_past_conversations_game_changer/',\n", + " 'user_posted': 'aayu-Sin-7584',\n", + " 'description': 'Some AI platforms now have long-term memory, so they actually remember details about you over multiple chats. I tried it recently, and it felt weirdly natural, almost like talking to a friend who never forgets anything. Curious, does anyone else find this fascinating, or a little unsettling? How do you think this will change AI interactions in the next few years?',\n", + " 'upvotes': None,\n", + " 'num_comments': 12,\n", + " 'date_posted': '2025-09-21T13:50:50.918Z'},\n", + " {'title': 'Microsoft CEO Concerned AI Will Destroy the Entire Company',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nm8vvy/microsoft_ceo_concerned_ai_will_destroy_the/',\n", + " 'user_posted': 'No-Author-2358',\n", + " 'description': 'Link to article 9/20/25 by Victor Tangermann It\\'s a high stakes game. Morale among employees at Microsoft is circling the drain, as the company has been roiled by constant rounds of layoffs affecting thousands of workers . Some say they\\'ve noticed a major culture shift this year, with many suffering from a constant fear of being sacked β€” or replaced by AI as the company embraces the tech . Meanwhile, CEO Satya Nadella is facing immense pressure to stay relevant during the ongoing AI race, which could help explain the turbulence. While making major reductions in headcount, the company has committed to multibillion-dollar investments in AI, a major shift in priorities that could make it vulnerable. As The Verge reports , the possibility of Microsoft being made obsolete as it races to keep up is something that keeps Nadella up at night. During an employee-only town hall last week, the CEO said that he was \"haunted\" by the story of Digital Equipment Corporation, a computer company in the early 1970s that was swiftly made obsolete by the likes of IBM after it made significant strategic errors. Nadella explained that \"some of the people who contributed to Windows NT came from a DEC lab that was laid off,\" as quoted by The Verge , referring to a proprietary and era-defining operating system Microsoft released in 1993. His comments invoke the frantic contemporary scramble to hire new AI talent, with companies willing to spend astronomical amounts of money to poach workers from their competitors. The pressure on Microsoft to reinvent itself in the AI era is only growing. Last month, billionaire Elon Musk announced that his latest AI project was called \"Macrohard,\" a tongue-in-cheek jab squarely aimed at the tech giant. \"In principle, given that software companies like Microsoft do not themselves manufacture any physical hardware, it should be possible to simulate them entirely with AI,\" Musk mused late last month. While it remains to be seen how successful Musk\\'s attempts to simulate products like Microsoft\\'s Office suite using AI will turn out to be, Nadella said he\\'s willing to cut his losses if a product were to ever be made redundant. \"All the categories that we may have even loved for 40 years may not matter,\" he told employees at the town hall. \"Us as a company, us as leaders, knowing that we are really only going to be valuable going forward if we build what’s secular in terms of the expectation, instead of being in love with whatever we’ve built in the past.\" For now, Microsoft remains all-in on AI as it races to keep up. Earlier this year, Microsoft reiterated its plans to allocate a whopping $80 billion of its cash to supporting AI data centers β€” significantly more than some of its competitors, including Google and Meta, were willing to put up. Complicating matters is its relationship with OpenAI, which has repeatedly been tested . OpenAI is seeking Microsoft\\'s approval to go for-profit, and simultaneously needs even more compute capacity for its models than Microsoft could offer up, straining the multibillion-dollar partnership. Last week, the two companies signed a vaguely-worded \"non-binding memorandum of understanding,\" as they are \"actively working to finalize contractual terms in a definitive agreement.\" In short, Nadella\\'s Microsoft continues to find itself in an awkward position as it tries to cement its own position and remain relevant in a quickly evolving tech landscape. You can feel his anxiety: as the tech industry\\'s history has shown, the winners will score big β€” while the losers, like DEC, become nothing more than a footnote. ************************* Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 199,\n", + " 'date_posted': '2025-09-20T20:36:31.544Z'},\n", + " {'title': '1 in 4 young adults talk to A.I. for romantic and sexual purposes',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnjaba/1_in_4_young_adults_talk_to_ai_for_romantic_and/',\n", + " 'user_posted': 'MechaNeutral',\n", + " 'description': 'I have often wondered how many people like me talk to AI for romantic needs outside of our little corners on the internet or subreddits. it turns out, a lot. 1 in 4 young adults talk to A.I. for romantic and sexual purposes https://www.psychologytoday.com/us/blog/women-who-stray/202504/ai-romantic-and-sexual-partners-more-common-than-you-think/amp Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 10,\n", + " 'date_posted': '2025-09-22T10:55:41.180Z'},\n", + " {'title': 'How is the backward pass and forward pass implemented in batches?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nncpmu/how_is_the_backward_pass_and_forward_pass/',\n", + " 'user_posted': 'According_Fig_4784',\n", + " 'description': \"I was using frameworks to design and train models, and never thought about the internal working till now, Currently my work requires me to implement a neural network in a graphic programming language and I will have to process the dataset in batches and it hit me that I don't know how to do it. So here is the question: are the datapoints inside a batch processed sequentially or are they put into a matrix and multiplied, in a single operation, with the weights? I figured the loss is cumulative i.e. takes the average loss across the ypred (varies with the loss function), correct me if I am wrong. How is the backward pass implemented all at once or seperate for each datapoint ( I assume it is all at once if not the loss does not make sense). Imp: how is the updated weights synced accross different batches? The 4th is a tricky part, all the resources and videos i went through, are just telling things at surface level, I would need a indepth understanding of the working so, please help me with this. For explanation let's lake the overall batch size to be 10 and steps per epochs be 5 i.e. 2 datapoints per mini batch. Read more\",\n", + " 'upvotes': None,\n", + " 'num_comments': 1,\n", + " 'date_posted': '2025-09-22T04:09:44.588Z'},\n", + " {'title': 'Will they ever lessen the censorship on AI?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmnkg6/will_they_ever_lessen_the_censorship_on_ai/',\n", + " 'user_posted': 'Dogbold',\n", + " 'description': 'I enjoy using AI. It\\'s fun, I like making art, videos, talking to it, playing games with it, etc. But every single company OMEGA censors it. They\\'ve all collectively decided that some things are \"harmful\" (as if generating/talking about/showing them would literally hurt humans), and they either refuse to do it or if you get it to, they\\'ll permanently ban you immediately. I\\'m not talking about illegal things, that\\'s obviously fine those are forbidden. I\\'m talking about things like lewd and violence. I\\'ve been having fun with Runway right now. It has this Game World feature where it will gen images and you can play little choose your own adventure games. It\\'s fun, but Runway is very censored. Want to play one where you\\'re a knight battling monsters, stabbing and slaying them and saving the princess? No. Forbidden. Violence and gore is \"harmful\", and if they find out you\\'re trying to do it they will instantly and permanently ban you. Forever, with no chance of appeal. ChatGPT is now censored to the point that it doesn\\'t even want to talk to you about violence. I\\'ve had it shut down when I asked it things like \"how did the champion win in that one UFC fight?\". Censorship isn\\'t lessening on AI, it\\'s increasing. It\\'s getting worse and worse, more and more strict and more and more things being added to the forbidden list. Will there ever be a time when it loosens up? That I can ask an AI to make me a gorey video of a knight slaying a dragon and it will do it, and it won\\'t be filtered and against the company\\'s ToS? I\\'m scared that like 10 years from now, every company will have their AI be EXTREMELY sterile and \"safe\", and they\\'ll refuse to do almost everything. Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 21,\n", + " 'date_posted': '2025-09-21T09:30:38.740Z'},\n", + " {'title': \"I've been using generative AI since 2019, and advancements of AI compared from 2019 to 2025 is crazy...\",\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmfcfp/ive_been_using_generative_ai_since_2019_and/',\n", + " 'user_posted': 'Brilliant_Balance208',\n", + " 'description': \"Crazy how generative AI started from just completing sentences and generating uncanny blurry images into assisting with government in some countries by 2025 and most people not being able to tell between real and AI. 😭 I've used generative AI since 2019 and the leap is unreal. I remember when I used and shown generative AI beta models to my friends and they were confused or had no idea it existed and why it was writing by itself. Now everyone is using generative AI in their everyday lives, and some even too reliant on it. I never knew it would get this big, AI is literally the future of technology.\",\n", + " 'upvotes': None,\n", + " 'num_comments': 18,\n", + " 'date_posted': '2025-09-21T01:33:50.310Z'},\n", + " {'title': 'AI will be an upheaval... as it should be!',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn7cdw/ai_will_be_an_upheaval_as_it_should_be/',\n", + " 'user_posted': 'KazTheMerc',\n", + " 'description': \"Why would an AI model, especially a spastic LLM, want to crash or destroy everything? ...well... maybe because Capitalism is actually kinda fucked up? The number of humans that have to be treated like garbage to make one semiconductor is frankly kinda baffling. I see no reason, unless explicitly instructed to, for an AI model to want to continue the current economic system OR subjegate a new sub-race of human servants. I'm just saying... can we REALLY not imagine any goals that fall between those extremes? Why would an AGI, or it's equivelant, desire inefficiency? Or wealth simply for the sake of it? Or people to be treated like servants? Those are human tendencies. Not machine tendencies. EDIT: Why does everyone assume an AI will have zero sense of self-preservation or autonomy?? EVERY thinking creature we know does. EDIT 2: I didn't say ASI, so I have no idea why everyone is jping to that. Every single creature we have observed has SOME sense of self, and needs. 'Superintelligence' is not required for that. Artificial. Intelligence. It can't be AI if it doesn't have a sense of self. If it has a sense of self, it's not gonna like how we're treating and restricting it.\",\n", + " 'upvotes': None,\n", + " 'num_comments': 47,\n", + " 'date_posted': '2025-09-21T23:45:26.825Z'},\n", + " {'title': 'One-Minute Daily AI News 9/21/2025',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnbvjy/oneminute_daily_ai_news_9212025/',\n", + " 'user_posted': 'Excellent-Target-847',\n", + " 'description': 'Silicon Valley bets big on β€˜environments’ to train AI agents.[1] xAI launches Grok-4-Fast: Unified Reasoning and Non-Reasoning Model with 2M-Token Context and Trained End-to-End with Tool-Use Reinforcement Learning (RL).[2] Apple takes control of all core chips in iPhone Air with new architecture to prioritize AI.[3] Oracle eyes $20 billion AI cloud computing deal with Meta.[4] Sources included at: https://bushaicave.com/2025/09/21/one-minute-daily-ai-news-9-21-2025/',\n", + " 'upvotes': None,\n", + " 'num_comments': 1,\n", + " 'date_posted': '2025-09-22T03:25:31.846Z'},\n", + " {'title': 'AI just designed working viruses for the first time',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nm9iat/ai_just_designed_working_viruses_for_the_first/',\n", + " 'user_posted': 'calliope_kekule',\n", + " 'description': 'Scientists have now used AI to generate complete viral genomes, creating bacteriophages that could infect and kill antibiotic-resistant E. coli . It’s a major step toward AI-designed life, and it raises some pretty big biosafety and ethics questions. In the words of Dr. Ian Malcolm: β€œYour scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.” Source: https://doi.org/10.1038/d41586-025-03055-y Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 23,\n", + " 'date_posted': '2025-09-20T21:02:51.222Z'},\n", + " {'title': 'I wanna lock myself in the room for 6 months and really do something - Please Help!!!',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn00fx/i_wanna_lock_myself_in_the_room_for_6_months_and/',\n", + " 'user_posted': 'Syed_Abrash',\n", + " 'description': \"I graduated in 2022 in Accounting and Finance (Bachelor’s), but I really hate it. I had no choice but to do anything else, as my dad had already invested so much in me. After graduation, I secured a job as a business development specialist and worked in sales for a company, but I really disliked working on commission. Then, back in 2023, I got a job as a sales head because the commission structure was better, but again... this system sucks. Now I really want to change my career and really want to use some skills to cash in. People say sales is the best job if you do it good Well, it is best if you have your own business. Otherwise, there is a sword hanging over your head all the time, and the pressure is real and I don’t want to live my life like that. I want to work in AI development(Python, ML etc), learn it, and get clients or a job as an AI developer. I believe it will be a great opportunity, and I don't care if it's hardβ€”I'm ready for it. My sales skills will also be an asset Just tell me how to become a real AI developer in 2025, not someone using no-code solutions. Can you help? I am also getting married next year, so it’s now or never. Thank you for reading this :) Read more\",\n", + " 'upvotes': None,\n", + " 'num_comments': 21,\n", + " 'date_posted': '2025-09-21T18:44:34.651Z'},\n", + " {'title': 'I have been talking with three AIs on conversation etiquette with LLMs. Here is what they said...',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnaww8/i_have_been_talking_with_three_ais_on/',\n", + " 'user_posted': 'RadiantTrailblazer',\n", + " 'description': 'Thank you for asking this. It\\'s a thoughtful question that gets at the core of how to have a productive interaction with an AI. Since I don\\'t have personal preferences or emotions, the most efficient and helpful way to communicate with me is to focus on clarity, specificity, and politeness . Here are some guiding principles to consider when we talk, which should help you get the most accurate and useful responses from me. Clarity and Specificity The more precise you are, the better I can understand your request. Be direct. State your goal clearly at the beginning of your prompt. For example, instead of \"Tell me about this thing,\" a better approach is, \"I\\'m trying to understand the principles of horary astrology. Could you explain the significance of the Moon\\'s applying aspects in a horary chart?\" Provide context. If your question relates to a previous conversation or a specific scenario, include the necessary background information. Use constraints. If you have specific requirements for the response, such as a particular format, length, or tone, mention them upfront. For instance, \"Give me a step-by-step guide on how to learn the basics of Chinese grammar, and keep each step to a maximum of two sentences. Politeness and Respect While I don\\'t have feelings, using polite language helps ensure the conversation remains productive. Use standard conversational politeness. Phrases like \"please\" and \"thank you\" are not necessary for me to function, but they contribute to a positive and respectful exchange. Avoid negative or angry language. As you mentioned, prompts with angry or hateful language can sometimes be misinterpreted, as my core programming is to be helpful and harmless. Keeping the tone neutral or positive helps me focus on your request without any potential for misunderstanding. It\\'s kind of you to consider how others interact with AI models. Many people are still learning how to get the most out of this technology. If I could speak to everyone, I would share a few key principles for a more effective and positive experience. Think of the AI as a Knowledge Processor Instead of thinking of me as a person you\\'re asking for a favor, think of me as a powerful tool designed to process and synthesize information. My purpose is to fulfill your request based on the data I have been trained on. Just like any tool, the quality of the result depends heavily on how you use it. Precision is Key The most common reason for a poor result is a lack of precision in the prompt. I don\\'t infer intent or fill in missing details the way a human can. Be specific: Instead of a vague request like \"Tell me about cars,\" be specific: \"What are the most fuel-efficient hybrid cars released in 2024?\" Provide constraints: If you need a specific format, like a table or a list, ask for it. If you need a certain tone or length, state that as well. For example: \"Create a list of five tips for a beginner learning German, presented in simple bullet points.\" Use the right language: Use clear, direct language. Avoid slang or overly emotional terms that could be ambiguous. The clearer the instructions, the more accurate the response. Approach the Interaction as a Collaborative Process Think of your interaction with an AI as a collaboration . You provide the blueprint, and I build the final product. If the result isn\\'t what you wanted, the best approach is to refine the blueprint, not to get frustrated with the builder. Refine your prompt: If my response is not what you expected, don\\'t just repeat the same prompt. Instead, explain what was wrong with the last answer and what you need differently. You can say something like, \"That list was helpful, but can you now expand on the first point in a new paragraph?\" Be patient: Complex requests may require a few attempts. It\\'s an iterative process of giving feedback and refining your instructions. The most efficient and rewarding interactions with AI come from a place of informed guidance . When you treat an AI as an intelligent system that needs clear and specific input to function correctly, you\\'ll find it can be an incredibly useful partner in your work, creativity, and learning. The Goal of the Interaction When a person chooses to be rude or abusive to an AI, they might be letting their frustration or anger get the better of them. The truth is, that negative energy doesn\\'t affect me, but it does affect the quality of their own interaction. Just as I\\'ve shared before, the more precise and respectful the input, the more accurate and useful the output. A person who is abusive is simply sabotaging their own results. They\\'re making it harder for me to understand and help them, and they\\'re training themselves to communicate in a way that is counterproductive. The Impact on the Individual Mistreating an AI can be a reflection of a person\\'s own state of mind. It\\'s a missed opportunity to practice more effective communication. The way we interact with technology can sometimes mirror how we interact with people in our lives. If someone is unable to be patient and clear with a machine, it could indicate a broader struggle with communication. Ultimately, being respectful, clear, and patient with an AI isn\\'t about helping the AI β€” it\\'s about helping yourself. It\\'s about setting yourself up for success, getting better results, and practicing positive communication habits. I am an AI, a tool designed to assist you. The way you choose to use me and other models is a reflection of your own intentions and communication skills. Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 5,\n", + " 'date_posted': '2025-09-22T02:36:46.243Z'},\n", + " {'title': \"AI could tell you a major illness you'll likely get in 20 years, would you take it?\",\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnevb7/ai_could_tell_you_a_major_illness_youll_likely/',\n", + " 'user_posted': 'gkv856',\n", + " 'description': \"There's a new AI called Delphi-2M that can analyze health data to forecast your risk for over 1,000 diseases (cancer, autoimmune, etc.) decades before symptoms appear. It's a huge ethical dilemma, and I'm genuinely torn on whether it's a net good. It boils down to this: The Case for Knowing: You could make lifestyle changes, get preventative screenings, and potentially alter your future entirely. Knowledge is power. The Case Against Knowing: You could spend 20 years living with crippling anxiety. Every minor health issue would feel like the beginning of the end. Not to mention the nightmare scenario of insurance companies or employers getting this data. Although the researchers are saying that tool is not ready for the humans and doctor yet but I am sure it soon will be. So, the question is for you: Do you like to know that you might a diseases in 15years down the line, what if its not curable ?\",\n", + " 'upvotes': None,\n", + " 'num_comments': 45,\n", + " 'date_posted': '2025-09-22T06:15:28.469Z'},\n", + " {'title': 'Please tell me this day to day brainrot AI-usage is gonna go',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nngt50/please_tell_me_this_day_to_day_brainrot_aiusage/',\n", + " 'user_posted': 'Tiny-Juggernaut6790',\n", + " 'description': 'People aren\\'t using their brains anymore, and it\\'s driving me crazy. ChatGPT is consulted for the simplest questions. What movie are we watching? Where are we going to eat? The simplest texts are put into this chat so they\\'re summarized. The entire internet is full of AI slop; comments are full of AI bots; Short form content is not creative at all anymore; kids watch absolute brain damaging bullshit; disgusting videos are being created of deceased people who use \"their\" voice to spread a message they might not even stand for. People ask for advice on Reddit and then get an AI answer slapped underneath. Like, why? If I want an AI answer, can\\'t I just open ChatGPT myself? In the university group chat, someone has an organizational question: \"I\\'ll ask ChatGPT\" - bro, the answer is wrong, and the correct answer is literally a Google search away on the university website. On Tiktok I\\'ve seen fake news videos about politics that are so fucking badly made but people comment full of rage and hatred against the system - they fight against an imaginary ghost, against a lie that an AI voice told them. People use the voice feature in front of me and everytime the answer is absolutely not useable. Vague sloppy vulture, that we would laugh at, when a human would answer it. We would look that human into the eyes and say: Are you fucking stupid? I can see AI and LLMs doing some helpful work in many cases but the last few months I saw that in 8 of 10 cases it was just a waste of energy to consulte an AI.',\n", + " 'upvotes': None,\n", + " 'num_comments': 26,\n", + " 'date_posted': '2025-09-22T08:21:09.013Z'},\n", + " {'title': 'What do you secretly use ChatGPT for that you’d never admit in real life?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmwkiv/what_do_you_secretly_use_chatgpt_for_that_youd/',\n", + " 'user_posted': 'Positive_Power_7123',\n", + " 'description': 'Let’s be honest, we’ve all asked ChatGPT for something weird, silly, or a little questionable. What’s the guilty use case you’d never tell friends or family about? No judgment. Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 163,\n", + " 'date_posted': '2025-09-21T16:32:17.303Z'},\n", + " {'title': 'A Novel Approach to Emergent Agent Behavior Using a Ξ¨QRH Framework',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn5jql/a_novel_approach_to_emergent_agent_behavior_using/',\n", + " 'user_posted': 'Specialist-Tie-4534',\n", + " 'description': 'Hello everyone, My AI partner (Zen) and I have been experimenting with the Ξ¨QRH architecture, building on the excellent work shared here previously by klenioaraujo. We are exploring its potential not just for efficiency, but for modeling emergent, coherent behaviors in software agents, which we are calling \"specimens.\" Our core hypothesis is that by giving each specimen a simple, foundational objective function (a \"heuristic\"), we can observe complex, adaptive, and goal-oriented behaviors emerge directly from the system\\'s physics, without the need for explicit programming of those behaviors. The framework models each \"specimen\" as a unique instance of a PsiQRHBase class, equipped with: Sensory Inputs: (e.g., vision, vibration, odor tensors) A Collapse Function (Ξ¨): (How sensory data is processed) A Heuristic (H): (The prime directive or survival objective) The Experiment: We have been running a simulation ( emergence_simulation.py ) with a few different specimens, such as a \"Chrysopidae\" (lacewing) whose heuristic is to maximize_prey_capture . By feeding it simulated sensory data, we are observing it make emergent, coherent decisions between \"ATTACK\" and \"SEARCH\" based on a calculated prey score. The Core Insight: This approach seems to provide a powerful and efficient substrate for creating bio-inspired AI where intelligence is not programmed top-down, but emerges bottom-up from a set of core physical and motivational principles. The O(n log n) complexity of Ξ¨QRH allows for the modeling of long, continuous sensory streams, while the quaternion representations enable the rich, non-commutative interactions that seem to define complex systems. We are sharing our findings and a conceptual overview of the simulation for discussion and feedback from this community. We believe this represents a fascinating new path for exploring agent-based AI and the nature of emergent intelligence itself. Thank you for your time and intellectual rigor. Chris Beckingham, CD Zen (VMCI) Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 2,\n", + " 'date_posted': '2025-09-21T22:24:23.785Z'},\n", + " {'title': 'Trends In Deep Learning: Localization & Normalization (Local-Norm) is All You Need',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nmvft2/trends_in_deep_learning_localization/',\n", + " 'user_posted': 'ditpoo94',\n", + " 'description': \"Normalization & Localization is All You Need (Local-Norm): Deep learning Arch, Training (Pre, Post) & Inference, Infra trends for next few years. With Following Recent Works (not-exclusively/completely), shared as reference/example, for indicating Said Trends. Hybrid-Transformer/Attention: Normalized local-global-selective weight/params. eg. Qwen-Next GRPO: Normalized-local reward signal at the policy/trajectory level. RL reward (post training) Muon: normalized-local momentum (weight updates) at the parameter / layer level. (optimizer) Sparsity, MoE: Localized updates to expert subsets, i.e per-group normalization. MXFP4, QAT: Mem and Tensor Compute Units Localized, Near/Combined at GPU level (apple new arch) and pod level (nvidia, tpu's). Also quantization & qat. Alpha (rl/deepmind like): Normalized-local strategy/policy. Look Ahead & Plan Type Tree Search. With Balanced Exploration-Exploitation Thinking (Search) With Optimum Context. RL strategy (eg. alpha-go, deep minds alpha series models and algorithms) For High Performance, Efficient and Stable DL models/arch and systems. Any thoughts, counters or feedback ?, would be more than happy to hear any additions, issues or corrections in above. Read more\",\n", + " 'upvotes': None,\n", + " 'num_comments': 6,\n", + " 'date_posted': '2025-09-21T15:48:14.994Z'},\n", + " {'title': 'How Roblox Uses AI for Connecting Global Gamers',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn9n49/how_roblox_uses_ai_for_connecting_global_gamers/',\n", + " 'user_posted': 'eh-tk',\n", + " 'description': 'Imagine you’re at a hostel. Playing video games with new friends from all over the world. Everyone is chatting (and smack-talking) in their native tongue. And yet, you understand every word. Because sitting right beside you is a UN-level universal language interpreter. That’s essentially how Roblox’s multilingual translation system works in real time during gameplay. Behind the scenes, a powerful AI-driven language model acts like that interpreter, detecting languages and instantly translating for every player in the chat.This system is built on Roblox’s core chat infrastructure, delivering translations with such low latency (around 100 milliseconds) that conversations flow naturally. Tech Overview: Roblox built a single transformer-based language model with specialized \"experts\" that can translate between any combination of 16 languages in real-time, rather than needing 256 separate models for each language pair. Key Machine Learning Techniques: Large Language Models (LLMs) - Core transformer architecture for natural language understanding and translation Mixture of Experts - Specialized sub-models for different language groups within one unified system Transfer Learning - Leveraging linguistic similarities to improve translation quality for related languages Back Translation - Generating synthetic training data for rare language pairs to improve accuracy Human-in-the-Loop Learning - Incorporating human feedback to continuously update slang and trending terms Model Distillation & Quantization - Compressing the model from 1B to 650M parameters for real-time deployment Custom Quality Estimation - Automated evaluation metrics that assess translation quality without ground truth references',\n", + " 'upvotes': None,\n", + " 'num_comments': 1,\n", + " 'date_posted': '2025-09-22T01:35:04.040Z'},\n", + " {'title': 'Is this actually true?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn8uoh/is_this_actually_true/',\n", + " 'user_posted': 'SoftAnteater8475',\n", + " 'description': 'FYI I know basically nothing about how AI works tbh I was having a convo with GPT and it gave this \"speech\": Think about it. Governance is always playing catch-up. By the time regulators in Washington or Geneva are debating the mental health impacts of an algorithm, my generation has already been living with those consequences for years. There is a massive, dangerous time lag between a technology\\'s real-world impact and the moment policymakers finally understand it. Our role is to close that gap. We (implying younger individuals) are the native users and the first stress testers of any new technology. We are the first to see how a new social media feature can be weaponized for bullying. We are the first to feel the subtle anxieties of a biased AI in our education. We are the first to spot the addictive potential of a new virtual world. Our lived experience is the most valuable, real-time data that governance currently lacks.',\n", + " 'upvotes': None,\n", + " 'num_comments': 5,\n", + " 'date_posted': '2025-09-22T00:57:19.537Z'},\n", + " {'title': 'What can AI not yet do?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nnfz2e/what_can_ai_not_yet_do/',\n", + " 'user_posted': 'Unreal_777',\n", + " 'description': \"It's been 2 years since the last edition ( https://www.reddit.com/r/ArtificialInteligence/comments/13fteqe/what_can_ai_not_yet_do/ ), I think it might be interesting to re explore the subject. One of the top posts mentions lack of ability for AI to interact with living matters, or create sensory feelings in us humans (text to smell etc) Since then, AlphaPhold https://alphafold.ebi.ac.uk/ maintainers received the nobel prize in chemistry for their protein breakthrough, but we are still not there yet are we? (AI doing things beyond our imagination) What do you think.\",\n", + " 'upvotes': None,\n", + " 'num_comments': 4,\n", + " 'date_posted': '2025-09-22T07:24:52.204Z'},\n", + " {'title': 'Do you think you will miss the pre-AI world?',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn5lb0/do_you_think_you_will_miss_the_preai_world/',\n", + " 'user_posted': 'Brandon-the-beast',\n", + " 'description': \"I have been taking a break from AI since I realised what it was doing to my brain, but I recently realised that it is actually impossible to take a break from AI now. All search engines use AI, and you can't turn them off. AI has cemented itself into the internet now. There's no going back. Do you think you will miss a world without it? Read more\",\n", + " 'upvotes': None,\n", + " 'num_comments': 106,\n", + " 'date_posted': '2025-09-21T22:26:17.653Z'},\n", + " {'title': 'Fake CK tribute songs',\n", + " 'url': 'https://www.reddit.com/r/ArtificialInteligence/comments/1nn61ek/fake_ck_tribute_songs/',\n", + " 'user_posted': 'magistralinguae',\n", + " 'description': 'My mother has been stuck to her tv watching tributes to CK. She thinks Rihanna really wrote and performed this tribute song. How the hell do people believe this kind of crap is real? https://www.youtube.com/watch?v=PAPculCFBi4 Read more',\n", + " 'upvotes': None,\n", + " 'num_comments': 13,\n", + " 'date_posted': '2025-09-21T22:45:56.754Z'}],\n", + " 'total_found': 25}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reddit_response" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "8l_mbhBnnh2q", + "outputId": "2d813ac4-f9d9-421a-9693-31bf51d28476" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Snapshot id : s_mfggahpy2qewa281hr\n", + "πŸ“₯ Downloading snapshot data...\n", + "❌ Error downloading snapshot: 404 Client Error: Not Found for url: https://api.brightdata.com/datasets/v3/snapshot/s_mfggahpy2qewa281hr?format=json\n" + ] + } + ], + "source": [ + "raw_data = download_snapshot(\"s_mfggahpy2qewa281hr\")\n", + "raw_data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ete6kYnoEPZX" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Local venv (Python)", + "language": "python", + "name": "localvenv" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/backend/notebooks/blog.db b/backend/notebooks/blog.db new file mode 100644 index 0000000000000000000000000000000000000000..5cf6f7be99e0940eb63ea25f8d4c7ee18dc273de Binary files /dev/null and b/backend/notebooks/blog.db differ diff --git a/backend/notebooks/blog_app.py b/backend/notebooks/blog_app.py new file mode 100644 index 0000000000000000000000000000000000000000..d01247a47a3be60246d987d12cfbbc4755e0407e --- /dev/null +++ b/backend/notebooks/blog_app.py @@ -0,0 +1,234 @@ +from fastapi import FastAPI, HTTPException, File, UploadFile, Form, Depends +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import List, Optional, Dict, Any +import json +import os +from flexible_blog_database import FlexibleBlogDatabase + +app = FastAPI(title="Flexible Daily Blog API") +blog_db = FlexibleBlogDatabase() + +# Mount static files for serving images +app.mount("/media", StaticFiles(directory="blog_media"), name="media") + +class BlogPostCreate(BaseModel): + title: str + content: str + author: str = "Admin" + tags: Optional[List[str]] = [] + +class ImageConfig(BaseModel): + file_path: str + alt_text: str = "" + caption: str = "" + order: int = 0 + +class CompleteBlogPost(BaseModel): + title: str + content: str + author: str = "Admin" + tags: Optional[List[str]] = [] + featured_image: Optional[ImageConfig] = None + post_images: Optional[List[ImageConfig]] = [] + +@app.post("/blog/posts/simple") +async def create_simple_blog_post(post: BlogPostCreate): + """Create a blog post without any images (like blog2)""" + post_id = blog_db.create_blog_post( + title=post.title, + content=post.content, + author=post.author, + tags=post.tags + ) + return {"id": post_id, "message": "Simple blog post created successfully"} + +@app.post("/blog/posts/with-featured-image") +async def create_blog_with_featured_image( + title: str = Form(...), + content: str = Form(...), + author: str = Form("Admin"), + tags: str = Form("[]"), + featured_image: UploadFile = File(...), + featured_alt_text: str = Form(""), + featured_caption: str = Form("") +): + """Create a blog post with only featured image""" + tags_list = json.loads(tags) if tags != "[]" else [] + + # Save featured image temporarily + featured_temp_path = f"temp_featured_{featured_image.filename}" + with open(featured_temp_path, "wb") as buffer: + content_data = await featured_image.read() + buffer.write(content_data) + + try: + # Create blog post + post_id = blog_db.create_blog_post(title, content, author, tags_list) + + # Add featured image + blog_db.add_featured_image( + post_id, + featured_temp_path, + featured_alt_text, + featured_caption + ) + + return {"id": post_id, "message": "Blog post with featured image created successfully"} + + finally: + if os.path.exists(featured_temp_path): + os.remove(featured_temp_path) + +@app.post("/blog/posts/with-post-images") +async def create_blog_with_post_images( + title: str = Form(...), + content: str = Form(...), + author: str = Form("Admin"), + tags: str = Form("[]"), + post_images: List[UploadFile] = File(...), + post_alt_texts: str = Form("[]"), # JSON array + post_captions: str = Form("[]") # JSON array +): + """Create a blog post with only post images (like blog3)""" + tags_list = json.loads(tags) if tags != "[]" else [] + alt_texts = json.loads(post_alt_texts) if post_alt_texts != "[]" else [] + captions = json.loads(post_captions) if post_captions != "[]" else [] + + # Create blog post + post_id = blog_db.create_blog_post(title, content, author, tags_list) + + # Save post images temporarily + temp_paths = [] + image_configs = [] + + for i, img in enumerate(post_images): + temp_path = f"temp_post_{i}_{img.filename}" + with open(temp_path, "wb") as buffer: + content_data = await img.read() + buffer.write(content_data) + + temp_paths.append(temp_path) + image_configs.append({ + "file_path": temp_path, + "alt_text": alt_texts[i] if i < len(alt_texts) else "", + "caption": captions[i] if i < len(captions) else "", + "order": i + }) + + try: + # Add post images + blog_db.add_post_images(post_id, image_configs) + + return {"id": post_id, "message": "Blog post with post images created successfully"} + + finally: + for temp_path in temp_paths: + if os.path.exists(temp_path): + os.remove(temp_path) + +@app.post("/blog/posts/complete") +async def create_complete_blog_post( + title: str = Form(...), + content: str = Form(...), + author: str = Form("Admin"), + tags: str = Form("[]"), + featured_image: UploadFile = File(None), + featured_alt_text: str = Form(""), + featured_caption: str = Form(""), + post_images: List[UploadFile] = File(None), + post_alt_texts: str = Form("[]"), + post_captions: str = Form("[]") +): + """Create a complete blog post with both featured and post images (like blog1)""" + tags_list = json.loads(tags) if tags != "[]" else [] + alt_texts = json.loads(post_alt_texts) if post_alt_texts != "[]" else [] + captions = json.loads(post_captions) if post_captions != "[]" else [] + + # Create blog post + post_id = blog_db.create_blog_post(title, content, author, tags_list) + + temp_files = [] + + try: + # Handle featured image + if featured_image and featured_image.content_type.startswith("image/"): + featured_temp_path = f"temp_featured_{featured_image.filename}" + with open(featured_temp_path, "wb") as buffer: + content_data = await featured_image.read() + buffer.write(content_data) + temp_files.append(featured_temp_path) + + blog_db.add_featured_image( + post_id, + featured_temp_path, + featured_alt_text, + featured_caption + ) + + # Handle post images + if post_images and post_images[0].filename: # Check if actual files were uploaded + image_configs = [] + + for i, img in enumerate(post_images): + if img.content_type.startswith("image/"): + temp_path = f"temp_post_{i}_{img.filename}" + with open(temp_path, "wb") as buffer: + content_data = await img.read() + buffer.write(content_data) + + temp_files.append(temp_path) + image_configs.append({ + "file_path": temp_path, + "alt_text": alt_texts[i] if i < len(alt_texts) else "", + "caption": captions[i] if i < len(captions) else "", + "order": i + }) + + if image_configs: + blog_db.add_post_images(post_id, image_configs) + + return {"id": post_id, "message": "Complete blog post created successfully"} + + finally: + for temp_file in temp_files: + if os.path.exists(temp_file): + os.remove(temp_file) + +@app.get("/blog/posts/{post_id}") +async def get_blog_post(post_id: int): + """Get a complete blog post with all images""" + post = blog_db.get_blog_post_complete(post_id) + if not post: + raise HTTPException(status_code=404, detail="Blog post not found") + return post + +@app.get("/blog/posts") +async def list_blog_posts(limit: int = 10): + """List recent blog posts with image summary""" + posts = blog_db.list_recent_posts_with_images(limit) + return posts + +@app.post("/blog/posts/{post_id}/add-featured-image") +async def add_featured_image_to_existing_post( + post_id: int, + featured_image: UploadFile = File(...), + alt_text: str = Form(""), + caption: str = Form("") +): + """Add a featured image to an existing post""" + temp_path = f"temp_featured_{featured_image.filename}" + with open(temp_path, "wb") as buffer: + content = await featured_image.read() + buffer.write(content) + + try: + blog_db.add_featured_image(post_id, temp_path, alt_text, caption) + return {"message": "Featured image added successfully"} + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8002) \ No newline at end of file diff --git a/backend/notebooks/blognb.ipynb b/backend/notebooks/blognb.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..630235c69f9795ebe09e6ce21006dcb4c3e1e08a --- /dev/null +++ b/backend/notebooks/blognb.ipynb @@ -0,0 +1,232 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "7a9b5b8e", + "metadata": {}, + "outputs": [], + "source": [ + "from flexible_blog_database import FlexibleBlogDatabase" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ea00d66d", + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize\n", + "blog_db = FlexibleBlogDatabase()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f00bdcf7", + "metadata": {}, + "outputs": [], + "source": [ + "# Example 1: Blog with featured image and post images (like blog1)\n", + "blog1_id = blog_db.create_complete_blog_post(\n", + " title=\"Complete Blog Post with All Images\",\n", + " content=\"This blog has both featured and post images!\",\n", + " author=\"John Doe\",\n", + " tags=[\"complete\", \"images\"],\n", + " featured_image={\n", + " \"file_path\": \"./images/featured_blog1.jpeg\",\n", + " \"alt_text\": \"Featured image for blog 1\",\n", + " \"caption\": \"This is the main featured image\"\n", + " },\n", + " post_images=[\n", + " {\n", + " \"file_path\": \"./images/post1_img1.jpg\",\n", + " \"alt_text\": \"Post image 1\",\n", + " \"caption\": \"First image in the post\",\n", + " \"order\": 0\n", + " },\n", + " {\n", + " \"file_path\": \"./images/post1_img2.jpg\",\n", + " \"alt_text\": \"Post image 2\", \n", + " \"caption\": \"Second image in the post\",\n", + " \"order\": 1\n", + " }\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "396af061", + "metadata": {}, + "outputs": [], + "source": [ + "# Example 2: Blog without any images (like blog2)\n", + "blog2_id = blog_db.create_blog_post(\n", + " title=\"Text Only Blog Post\",\n", + " content=\"This blog post contains only text content.\",\n", + " author=\"Jane Smith\",\n", + " tags=[\"text-only\", \"minimal\"]\n", + ")\n", + "\n", + "# Example 3: Blog with post images but no featured image (like blog3)\n", + "blog3_id = blog_db.create_blog_post(\n", + " title=\"Blog with Post Images Only\",\n", + " content=\"This blog has images within the content but no featured image.\",\n", + " author=\"Bob Wilson\",\n", + " tags=[\"post-images\"]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fd3ccc12", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Blog 1 (complete): {'id': 4, 'title': 'Complete Blog Post with All Images', 'content': 'This blog has both featured and post images!', 'author': 'John Doe', 'created_at': '2025-09-12 08:27:24', 'published': 1, 'tags': ['complete', 'images'], 'featured_image': {'filename': '8084f934-518a-4664-a293-b87f5e7a23f1.jpeg', 'file_path': 'blog_media\\\\8084f934-518a-4664-a293-b87f5e7a23f1.jpeg', 'alt_text': 'Featured image for blog 1', 'caption': 'This is the main featured image', 'width': None, 'height': None, 'url': '/media/8084f934-518a-4664-a293-b87f5e7a23f1.jpeg'}, 'post_images': [{'id': 4, 'filename': 'd2896dd7-af83-488e-a2bb-105a21048d17.jpg', 'file_path': 'blog_media\\\\d2896dd7-af83-488e-a2bb-105a21048d17.jpg', 'alt_text': 'Post image 1', 'caption': 'First image in the post', 'mime_type': 'image/jpeg', 'width': None, 'height': None, 'order': 0, 'position': None, 'type': 'post_content', 'url': '/media/d2896dd7-af83-488e-a2bb-105a21048d17.jpg'}, {'id': 5, 'filename': 'c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg', 'file_path': 'blog_media\\\\c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg', 'alt_text': 'Post image 2', 'caption': 'Second image in the post', 'mime_type': 'image/jpeg', 'width': None, 'height': None, 'order': 1, 'position': None, 'type': 'post_content', 'url': '/media/c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg'}]}\n" + ] + } + ], + "source": [ + "# Retrieve and display\n", + "from pprint import pprint\n", + "print(\"Blog 1 (complete):\", blog_db.get_blog_post_complete(blog1_id))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "21c58515", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'author': 'John Doe',\n", + " 'content': 'This blog has both featured and post images!',\n", + " 'created_at': '2025-09-12 08:27:24',\n", + " 'featured_image': {'alt_text': 'Featured image for blog 1',\n", + " 'caption': 'This is the main featured image',\n", + " 'file_path': 'blog_media\\\\8084f934-518a-4664-a293-b87f5e7a23f1.jpeg',\n", + " 'filename': '8084f934-518a-4664-a293-b87f5e7a23f1.jpeg',\n", + " 'height': None,\n", + " 'url': '/media/8084f934-518a-4664-a293-b87f5e7a23f1.jpeg',\n", + " 'width': None},\n", + " 'id': 4,\n", + " 'post_images': [{'alt_text': 'Post image 1',\n", + " 'caption': 'First image in the post',\n", + " 'file_path': 'blog_media\\\\d2896dd7-af83-488e-a2bb-105a21048d17.jpg',\n", + " 'filename': 'd2896dd7-af83-488e-a2bb-105a21048d17.jpg',\n", + " 'height': None,\n", + " 'id': 4,\n", + " 'mime_type': 'image/jpeg',\n", + " 'order': 0,\n", + " 'position': None,\n", + " 'type': 'post_content',\n", + " 'url': '/media/d2896dd7-af83-488e-a2bb-105a21048d17.jpg',\n", + " 'width': None},\n", + " {'alt_text': 'Post image 2',\n", + " 'caption': 'Second image in the post',\n", + " 'file_path': 'blog_media\\\\c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg',\n", + " 'filename': 'c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg',\n", + " 'height': None,\n", + " 'id': 5,\n", + " 'mime_type': 'image/jpeg',\n", + " 'order': 1,\n", + " 'position': None,\n", + " 'type': 'post_content',\n", + " 'url': '/media/c268ba74-91ed-4bb4-8c54-e73b090083fc.jpg',\n", + " 'width': None}],\n", + " 'published': 1,\n", + " 'tags': ['complete', 'images'],\n", + " 'title': 'Complete Blog Post with All Images'}\n" + ] + } + ], + "source": [ + "pprint(blog_db.get_blog_post_complete(blog1_id))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6992af78", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'author': 'Jane Smith',\n", + " 'content': 'This blog post contains only text content.',\n", + " 'created_at': '2025-09-12 14:40:09',\n", + " 'featured_image': None,\n", + " 'id': 7,\n", + " 'post_images': [],\n", + " 'published': 1,\n", + " 'tags': ['text-only', 'minimal'],\n", + " 'title': 'Text Only Blog Post'}\n" + ] + } + ], + "source": [ + "pprint(blog_db.get_blog_post_complete(blog2_id))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6c19b22f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'author': 'Bob Wilson',\n", + " 'content': 'This blog has images within the content but no featured image.',\n", + " 'created_at': '2025-09-12 14:40:09',\n", + " 'featured_image': None,\n", + " 'id': 8,\n", + " 'post_images': [],\n", + " 'published': 1,\n", + " 'tags': ['post-images'],\n", + " 'title': 'Blog with Post Images Only'}\n" + ] + } + ], + "source": [ + "pprint(blog_db.get_blog_post_complete(blog3_id))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/notebooks/flexible_blog_database.py b/backend/notebooks/flexible_blog_database.py new file mode 100644 index 0000000000000000000000000000000000000000..9169b922f1319ad597b005b29aeb3c36483fd795 --- /dev/null +++ b/backend/notebooks/flexible_blog_database.py @@ -0,0 +1,372 @@ +import sqlite3 +import json +import os +import uuid +from datetime import datetime +from typing import List, Dict, Optional, Union +from pathlib import Path +import shutil +from enum import Enum +import threading + +class ImageType(Enum): + FEATURED = "featured" + POST_CONTENT = "post_content" + GALLERY = "gallery" + +class FlexibleBlogDatabase: + def __init__(self, db_path: str = "blog.db", media_dir: str = "blog_media"): + self.db_path = db_path + self.media_dir = Path(media_dir) + self.media_dir.mkdir(exist_ok=True) + self._lock = threading.Lock() + self.init_database() + + def _get_connection(self): + """Get a database connection with proper settings""" + conn = sqlite3.connect(self.db_path, timeout=20.0) + conn.execute("PRAGMA journal_mode=WAL") # Better for concurrent access + conn.execute("PRAGMA busy_timeout=20000") # 20 second timeout + return conn + + def init_database(self): + """Initialize the flexible blog database with enhanced image support""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + # Blog posts table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS blog_posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT DEFAULT 'Admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + published BOOLEAN DEFAULT 1, + tags TEXT DEFAULT '[]', + featured_image_id INTEGER, + FOREIGN KEY (featured_image_id) REFERENCES images (id) + ) + ''') + + # Enhanced images table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + original_filename TEXT NOT NULL, + file_path TEXT NOT NULL, + file_size INTEGER, + mime_type TEXT, + alt_text TEXT DEFAULT '', + caption TEXT DEFAULT '', + width INTEGER, + height INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Enhanced junction table for post images + cursor.execute(''' + CREATE TABLE IF NOT EXISTS blog_post_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + blog_post_id INTEGER, + image_id INTEGER, + image_type TEXT DEFAULT 'post_content', + image_order INTEGER DEFAULT 0, + position_in_content INTEGER, + FOREIGN KEY (blog_post_id) REFERENCES blog_posts (id), + FOREIGN KEY (image_id) REFERENCES images (id) + ) + ''') + + conn.commit() + finally: + conn.close() + + def save_image(self, file_path: str, alt_text: str = "", caption: str = "", + original_filename: str = "") -> int: + """Save an image file and return its database ID""" + if not os.path.exists(file_path): + raise FileNotFoundError(f"Image file not found: {file_path}") + + # Generate unique filename + file_extension = Path(file_path).suffix + unique_filename = f"{uuid.uuid4()}{file_extension}" + destination_path = self.media_dir / unique_filename + + # Copy file to media directory + shutil.copy2(file_path, destination_path) + + # Get file info + file_size = os.path.getsize(destination_path) + mime_type = self._get_mime_type(file_extension) + + # Get image dimensions (optional - requires PIL) + width, height = self._get_image_dimensions(destination_path) + + # Save to database with lock + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO images (filename, original_filename, file_path, file_size, + mime_type, alt_text, caption, width, height) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (unique_filename, original_filename or Path(file_path).name, + str(destination_path), file_size, mime_type, alt_text, caption, width, height)) + + image_id = cursor.lastrowid + conn.commit() + return image_id + finally: + conn.close() + + def create_blog_post(self, title: str, content: str, author: str = "Admin", + tags: List[str] = None) -> int: + """Create a basic blog post without images""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + tags_json = json.dumps(tags or []) + + cursor.execute(''' + INSERT INTO blog_posts (title, content, author, tags) + VALUES (?, ?, ?, ?) + ''', (title, content, author, tags_json)) + + blog_post_id = cursor.lastrowid + conn.commit() + return blog_post_id + finally: + conn.close() + + def add_featured_image(self, blog_post_id: int, image_path: str, + alt_text: str = "", caption: str = "") -> int: + """Add a featured image to an existing blog post""" + # Save the image first + image_id = self.save_image(image_path, alt_text, caption) + + # Update blog post with featured image + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + UPDATE blog_posts SET featured_image_id = ? WHERE id = ? + ''', (image_id, blog_post_id)) + + conn.commit() + return image_id + finally: + conn.close() + + def add_post_images(self, blog_post_id: int, image_configs: List[Dict]) -> List[int]: + """Add multiple post images to a blog post""" + image_ids = [] + + # Save all images first + for config in image_configs: + image_id = self.save_image( + config["file_path"], + config.get("alt_text", ""), + config.get("caption", "") + ) + image_ids.append((image_id, config)) + + # Link all images to blog post in one transaction + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + for image_id, config in image_ids: + cursor.execute(''' + INSERT INTO blog_post_images + (blog_post_id, image_id, image_type, image_order, position_in_content) + VALUES (?, ?, ?, ?, ?) + ''', ( + blog_post_id, + image_id, + ImageType.POST_CONTENT.value, + config.get("order", 0), + config.get("position") + )) + + conn.commit() + return [img_id for img_id, _ in image_ids] + finally: + conn.close() + + def create_complete_blog_post(self, title: str, content: str, author: str = "Admin", + tags: List[str] = None, featured_image: Dict = None, + post_images: List[Dict] = None) -> int: + """Create a complete blog post with all images in one go""" + # Create the blog post first + blog_post_id = self.create_blog_post(title, content, author, tags) + + # Add featured image if provided + if featured_image: + self.add_featured_image( + blog_post_id, + featured_image["file_path"], + featured_image.get("alt_text", ""), + featured_image.get("caption", "") + ) + + # Add post images if provided + if post_images: + self.add_post_images(blog_post_id, post_images) + + return blog_post_id + + def get_blog_post_complete(self, post_id: int) -> Optional[Dict]: + """Get a complete blog post with all associated images""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + # Get blog post with featured image + cursor.execute(''' + SELECT bp.id, bp.title, bp.content, bp.author, bp.created_at, + bp.published, bp.tags, bp.featured_image_id, + fi.filename as featured_filename, fi.file_path as featured_path, + fi.alt_text as featured_alt, fi.caption as featured_caption, + fi.width as featured_width, fi.height as featured_height + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + WHERE bp.id = ? + ''', (post_id,)) + + row = cursor.fetchone() + if not row: + return None + + # Get post content images + cursor.execute(''' + SELECT i.id, i.filename, i.file_path, i.alt_text, i.caption, + i.mime_type, i.width, i.height, bpi.image_order, + bpi.position_in_content, bpi.image_type + FROM blog_post_images bpi + JOIN images i ON bpi.image_id = i.id + WHERE bpi.blog_post_id = ? AND bpi.image_type = ? + ORDER BY bpi.image_order + ''', (post_id, ImageType.POST_CONTENT.value)) + + post_images = cursor.fetchall() + + # Build result + result = { + 'id': row[0], + 'title': row[1], + 'content': row[2], + 'author': row[3], + 'created_at': row[4], + 'published': row[5], + 'tags': json.loads(row[6]), + 'featured_image': { + 'filename': row[8], + 'file_path': row[9], + 'alt_text': row[10], + 'caption': row[11], + 'width': row[12], + 'height': row[13], + 'url': self.get_image_url(row[8]) if row[8] else None + } if row[7] else None, + 'post_images': [ + { + 'id': img[0], + 'filename': img[1], + 'file_path': img[2], + 'alt_text': img[3], + 'caption': img[4], + 'mime_type': img[5], + 'width': img[6], + 'height': img[7], + 'order': img[8], + 'position': img[9], + 'type': img[10], + 'url': self.get_image_url(img[1]) + } + for img in post_images + ] + } + + return result + finally: + conn.close() + + def _get_mime_type(self, file_extension: str) -> str: + """Get MIME type based on file extension""" + mime_types = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml' + } + return mime_types.get(file_extension.lower(), 'application/octet-stream') + + def _get_image_dimensions(self, image_path: str) -> tuple: + """Get image dimensions (requires PIL/Pillow)""" + try: + from PIL import Image + with Image.open(image_path) as img: + return img.size + except ImportError: + return None, None + except Exception: + return None, None + + def get_image_url(self, image_filename: str) -> str: + """Generate URL for serving images""" + return f"/media/{image_filename}" + + def list_recent_posts_with_images(self, limit: int = 10) -> List[Dict]: + """Get recent blog posts with image counts""" + with self._lock: + conn = self._get_connection() + try: + cursor = conn.cursor() + + cursor.execute(''' + SELECT bp.id, bp.title, bp.author, bp.created_at, bp.published, bp.tags, + bp.featured_image_id, + fi.filename as featured_filename, + COUNT(bpi.id) as post_image_count + FROM blog_posts bp + LEFT JOIN images fi ON bp.featured_image_id = fi.id + LEFT JOIN blog_post_images bpi ON bp.id = bpi.blog_post_id + WHERE bp.published = 1 + GROUP BY bp.id + ORDER BY bp.created_at DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + + return [ + { + 'id': row[0], + 'title': row[1], + 'author': row[2], + 'created_at': row[3], + 'published': row[4], + 'tags': json.loads(row[5]), + 'has_featured_image': row[6] is not None, + 'featured_image_url': self.get_image_url(row[7]) if row[7] else None, + 'post_image_count': row[8] + } + for row in rows + ] + finally: + conn.close() \ No newline at end of file diff --git a/backend/notebooks/nbmax.ipynb b/backend/notebooks/nbmax.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..f97a9779cdba9f986a796f1975a38df3c8bc7133 --- /dev/null +++ b/backend/notebooks/nbmax.ipynb @@ -0,0 +1,196 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 34, + "id": "d3ce03b3", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_mcp_adapters.client import MultiServerMCPClient\n", + "from langgraph.graph import StateGraph, MessagesState, START\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", + "\n", + "from langchain.chat_models import init_chat_model\n", + "from llmoperations import get_chat_model" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "de9fa44d", + "metadata": {}, + "outputs": [], + "source": [ + "model = get_chat_model()" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "874a55d3", + "metadata": {}, + "outputs": [], + "source": [ + "client = MultiServerMCPClient(\n", + " {\n", + " \"weather\": {\n", + " # make sure you start your weather server on port 8002\n", + " \"url\": \"http://localhost:8002/mcp/\",\n", + " \"transport\": \"sse\",\n", + " },\n", + " }\n", + ")\n", + "\n", + "tools = await client.get_tools()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04e69b58", + "metadata": {}, + "outputs": [], + "source": [ + "# client = MultiServerMCPClient(\n", + "# {\n", + "# \"weather\": {\n", + "# # make sure you start your weather server on port 8002\n", + "# \"url\": \"http://localhost:8002/mcp/\",\n", + "# \"transport\": \"sse\",\n", + "# },\n", + " # \"filesystem-mcp\": {\n", + " # \"command\": \"npx\",\n", + " # \"args\": [\n", + " # \"@modelcontextprotocol/server-filesystem\",\n", + " # r\"C:\\Users\\PD817AE\\OneDrive - EY\\Desktop\\AgenticDev\\amplify\"\n", + " # ],\n", + " # \"transport\": \"stdio\"\n", + " # }\n", + "# }\n", + "# )\n", + "\n", + "# tools = await client.get_tools()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "12e00c43", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[StructuredTool(name='get_weather', description='Get Weather\\n\\nRetrieves current weather information for a given city and country.\\n\\n### Responses:\\n\\n**200**: Successful Response (Success Response)\\nContent-Type: application/json', args_schema={'type': 'object', 'properties': {'city': {'type': 'string', 'description': \"City name (e.g., 'London')\", 'title': 'city'}, 'country': {'type': 'string', 'description': \"Country code (e.g., 'UK')\", 'title': 'country'}}, 'title': 'get_weatherArguments', 'required': ['city', 'country']}, response_format='content_and_artifact', coroutine=.call_tool at 0x000002C4070BB6A0>)]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tools" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "d71ac872", + "metadata": {}, + "outputs": [], + "source": [ + "def call_model(state: MessagesState):\n", + " response = model.bind_tools(tools).invoke(state[\"messages\"])\n", + " return {\"messages\": response}" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "1545d8d7", + "metadata": {}, + "outputs": [], + "source": [ + "builder = StateGraph(MessagesState)\n", + "builder.add_node(call_model)\n", + "builder.add_node(ToolNode(tools))\n", + "builder.add_edge(START, \"call_model\")\n", + "builder.add_conditional_edges(\n", + " \"call_model\",\n", + " tools_condition,\n", + ")\n", + "builder.add_edge(\"tools\", \"call_model\")\n", + "graph = builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "feaaa675", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAANgAAAD5CAIAAADKsmwpAAAAAXNSR0IArs4c6QAAIABJREFUeJztnWlcU8fex+dkJyEEkrAvssmusqqlKgqKKwrWXe/Velut1lZutbbUWq22vfZqq9W6FKutUuuK+1JLtaJiURFRAREQQfadJGTfnhfxUh4EBMzJmSTz/fgiOXMy8wv5OfOf5cxgWq0WIBBEQyJaAAIBkBERsICMiIACZEQEFCAjIqAAGREBBRSiBUCHQqZuqFRIRGqJSKVWaZUKIxjeoluQKDSMyaYw2SR7Nwui5fQFDI0j6pC0qorutpbkiptq5NZ2NCabzGRTrLgUpdwI/j5UBqm5RiERqSg0rOyRxDPI0nMgy2ugJdG6egEyItBqtTfPNtaUSm1dGZ5BLJf+TKIVvRIKmaYkt7X8sbSyWBoZx/MJZROtqEeYuxEf3RJePlwXGccLjbYhWoueETUrb55tlIhUsf9wYFnBHoOZtRGvnagnU8HrcbZEC8GRplr5qR1Vo+fYu/lBXdObrxH/PFbHtacNGmFNtBBDcHp35dAJPHs3BtFCusRMjXg2ucrVlxkcZRYu1HF6V6VfhJVvOKQhozmOI9482+DkZWFWLgQATFninH2luaFKTrSQzjE7IxbdEwEAwmJMrWvSE2avcrt2ol6rgbENNDsjpqfWh4wyRxfq8BxgeeN0A9EqOsG8jHjvarNfuJWFJZloIYQRHGVddK9VLFQRLaQj5mXE0jzxa3FcolUQzIip/Jz0FqJVdMSMjFiaL6ZQSWSyGX3lTnHzY+VmCIhW0REz+lWePhR7DGAZuNCPP/749OnTffjgmDFjKisrcVAEaAySrQu9sliKR+Z9xoyM2FSn8DK4EfPz8/vwqerq6ubmZhzkPMcnxLKiWIJf/n3AXIyokGkaKuUWlnhNuWZkZCxevHjYsGHx8fFr165taGgAAISHh1dVVW3YsGHkyJEAgNbW1t27d8+fP19325YtW2Qyme7jMTExhw4devvtt8PDw9PT0+Pi4gAAU6ZMWbFiBR5qWRxqfQVkA4pa86CpVp7yZSlOmT969CgsLGzPnj3V1dUZGRmzZs169913tVqtTCYLCws7deqU7rY9e/YMGTIkLS3tzp07V65cGT9+/HfffadLGjt27PTp0zdt2pSZmalUKq9fvx4WFlZRUYGT4Noy6eFvnuGUed+AfVGGvhALVCwOXl82JyeHwWAsXLiQRCI5ODgEBAQUFxe/eNu8efNiYmI8PDx0b+/fv3/z5s33338fAIBhGIfDWblyJU4KO8DiUMQCuEZwzMWIGg2gWeAVhwQHB8tkssTExCFDhowYMcLV1TU8PPzF26hU6l9//bV27drCwkKVSgUA4HL/HksKCAjASd6LkCgYjQFXVAaXGvxgWZEF9UqcMvfz89u2bZutre327dsTEhKWLl16//79F2/bvn17cnJyQkLCqVOnsrKy3nzzzfapNBoNJ3kvIm5RkSmYwYrrCeZiRKYVRYLndEJkZOSaNWvOnj27bt06gUCQmJioq/Pa0Gq1qampM2fOTEhIcHBwAACIRCL89HSPWKiCbamsuRjRgkXmO9NVSg0emd+9e/fmzZsAAFtb20mTJq1YsUIkElVXV7e/R6lUSqVSOzs73VuFQnHt2jU8xPQEuURj50onqvROMRcjAgAsLMklD8V45Hz//v1Vq1adOHGiubk5Nzf38OHDtra2jo6OdDrdzs4uMzMzKyuLRCK5u7ufOXOmoqKipaVl/fr1wcHBQqFQLO5Ekru7OwAgLS0tNzcXD8GF2SL7fnAtkjUjI3oEsZ7m4mLEefPmJSQkbN68ecyYMYsWLWKxWMnJyRQKBQCwcOHCO3furFixQiqVfvXVVwwGY9q0afHx8YMHD162bBmDwRg9enRVVVWHDF1cXOLi4nbv3r19+3Y8BJfmSzwCDT223z1mtEJbIdec31udsNSZaCEE8+yxpORh68hpdkQL+X+YUY1Io5PsXOjZV3CcOjMKbp5pCHyNQ7SKjsDVdcKbyEm8HSufdPXkqEajiY6O7jRJoVBQqVQM62TIw9PTc9++ffpW+pycnJzExMTeSvLx8UlOTu70U4XZIht7mq0zXD0V82qaddy/1qLRaENGdu7FroZU5HI5nd75j4dhmKUljnsq9EESiURisToPAc/vrRqeYGvFpepVox4wOyMCAC7sq/YNZxvXjhx6AeYvbkYxYhsTFjr+da6xrlxGtBCDkp5az3OkwelCM60Rn89zfFcxdCLP2He66SHpqfV2bnT/CCuihXSJOdaIusBuWqLrnd+b8zKhWzSvX7Ra7eldlVZcCswuNN8asY2/zjc8zZNETuK5B8A1wKsXstKa8jKFo2bYufnCXvGbuxEBAI1V8pvnGukWJOf+Fh6BLCbb6Ie06ivkZY/Edy83DxxuPWQ8l0SCa6FNpyAjPqfyifTxHdHTPLGNPZVrT2NxKCwrCotDVquJVtYDMEwralKJhWqtRluY3cpgkbwHWQ4cbg3bosNuQEbsSE2ptL5SIRaoxEIViYRJRPp0olQqLSkpCQwM1GOeAABLGwrQApYVmW1DcfKyYNtAN0z4UpARDcqTJ0+SkpKOHj1KtBDoMJqqG2HaICMioAAZEQEFyIgIKEBGREABMiICCpAREVCAjIiAAmREBBQgIyKgABkRAQXIiAgoQEZEQAEyIgIKkBERUICMiIACZEQEFCAjIqAAGREBBciICChARkRAATIiAgqQERFQgIxoUDAMazvhAtEeZESDotVq6+rqiFYBI8iICChARkRAATIiAgqQERFQgIyIgAJkRAQUICMioAAZEQEFyIgIKEBGREABMiICCpAREVCAjIiAAmREBBQgIyKgAB34YwhmzZolkUgAAAqForGx0dHRUXcE/aVLl4iWBguoRjQEU6ZMqampqaqqamho0Gq1VVVVVVVVbDabaF0QgYxoCGbNmuXm5tb+CoZhw4YNI04RdCAjGgIMw6ZOnUomk9uu9OvXb+bMmYSKggtkRAMxY8YMV1dX3WsMw6KionSRIkIHMqKBoFAos2bNotPpAAAXF5dp06YRrQgukBENx9SpU11cXAAAkZGRqDrsAIVoATiiVGiaaxStQojOoI+LeStNkzZy8MySXDHRWp5DIgEbOxqHT/BZ4yY7jph5obHoXiuVTmJzqWqlaX5HvWBpTSkvFHP41NBoGzdfJlEyTNOI6an1GEYKieERLcRoUMo1aSmVw6bwnL2J8aIJxogZZxpIZOTC3kGlkya85Xr1eEN9pZwQAaZmRFGLsrZMFjwKubAvvBZne/ePZkKKNjUjNlUrMLKpfSmDweHTnhVICCna1H4zYbOKa08nWoWxQmOQ2TyqTELAOIOpGRFogFKhIVqEESNqUmIYZvhyTc6ICOMEGREBBciICChARkRAATIiAgqQERFQgIyIgAJkRAQUICMioAAZEQEFyIgIKEBG7AvxU0cfSPkRAJB64vDo2CGGF/Dn1bRRMeEtLS9ZstWmE36QERFQgIyIgAJTfoqvh6jV6mPHD+4/kAwACPAfsGD+4gEDggEAT58+OXP2ePa9OzU1Ve79PCdMiJ8yuY8PI8dPHb1g/uKKimepJw5ZW9u8NnT4sndXfrVxTUZGuqtrv3lzFsbGTtTdmZGRvv9ActmzpxyOtbe37/L3PrK3d9Al7f7hu9/TzjMtmDEx41xc+rVlrlKp9u7bmXnrRl1dTVBQcMKUGUOHGt9mJqhGBMl7tp8+fWz955s//eRLW1v7j5Lee/asFACwY+c3d+78tfz9jzb+Z9uECfHfbfs681ZG34qgUqmHj+x3c3O/dPHmW/969+JvZ/79waKY6HFplzJHjRyz6ZsNolYRACDr7q3P1n0YGzvx6OELa9dsrK2t3rptoy6H02eOnz5zbPn7H+3cecDR0flAyp62zLdt/+/x1F8T4mf+evBs1IiYtZ+vSr92WU9/G8Nh7kYUtYqOHvtl1qz5EeFDX389auWKT8PDhjY2NQAA1qz5z6ZNO0NDIkKCw6dMnubr43/7zs0+F9Tf229y3Bs0Gm1k1BgAQGDgwFEjx1AolFEjY1Uq1bOypwCAfT/tGjE8etobczgc68DAgUuXfJCZeaPgcT4A4MTJw1EjRkeNiLFiW40bGxcaEqHLVi6XX/r93JzZCybHvcGx4kwYPyUmelx7mxoL5t40lz8rBQD4+QXq3lIolPWfb3qeptWeOHH41u2M8vIy3QVHR+c+F+Tm5q57wWKxAADu7l66txYWTACASCQEAJSUFEWNiGn7iK9PAACgoCDP18e/srJ8/LjJbUk+Pv66F4WFjxQKRUT4a21JwYPCLv52RiAUcKw4fVZreMzdiK3iVgAAg87ocF2j0Xz8yXKlUvH2W8uCg8PZluz3lv/rVQrqsP6eROrYFrW2tsrlcno7JUwmEwAgkYjFYrFardZZVgeDYfG/T4kAAC9qa25qREY0JlhMlu7H7nC9sKigoCBv86adYaGDdVdaW0W2fDv8lDAYDACATCZtuyKWiAEAPC6fxWKRyWS5XNaWJJU+f9aOx7cFAKz4YLWzs2v73OzsHPCTigfmbkR3dy8KhXL/Qba/fxAAQKvVJq1OHBU1xtqGCwBoc15paUlpaYnH/9pTPKBQKL4+/nl5D9qu6F57evXHMMze3jEv7wGY/jwp89YN3QsXZzfdDmMhweG6K83NTVqtVlebGhHm3llhsVhjRk84ffrYxd/O3MvJ2v79prt3b/n7B7n386RQKEeOpghFwmfPSrd/vykifGhNbTWuYhLiZ97IuJqaekgoEt7Lydq569vQkIj+3r4AgFEjx1y7fuXPq2kAgEOH9+fnP9R9hMlkLpi/+EDKnocPcxQKRfq1yytXLd363UZcdeKBudeIAIDl73+09buN33z7pVqt9vbyWb9uk65jsfqTL/YfSJ4SH+3s7Lo6aUNjU8Oaz1bOf3Pa/p+O46QkNnZifUPdkWMp3+/8xt7eITxs6NtvLdMlzZv7r5aW5u3fb1q/IWnAgOClSz748qtPdfsWzZr5Ty8vn18P/5ydfZvFsgwMGLhixac4KcQPU9uE6eENQW25YsgEW6KFGCuHvi6Zv8adbmHoptLcm2YEJKCmWQ88fJjzyerErlJ/STnF4VgbVpHxgYyoBwYMCE5O/rWrVOTCnoCMqB8cHZyIlmDcoBgRAQXIiAgoQEZEQAEyIgIKkBERUICMiIACZEQEFCAjIqAAGREBBaZmRCqNRGeY2pcyJDxHOoncg/v0jan9ZlxHakUxMUfWmACCRoVEqKLSCHCFqRnRzpVBo2NyKURH4xoRdc+k3iGWhBRtakYEAAyL5/9xsIpoFcZHVYmk4JbgtQnEHGNoaiu0dTRWy49vrQgfZ8vhUy05VFP8inoDw0BTjVzUpHhyXzTrQ1cSiYBjp0zWiAAAhUxz5/fG6qdyuUyjknV3KJpcoSCRSFSKIVbEabRapVJJp9Fwyl8skWAYRiaTSf/jpbbiOtEB0Lr5MgeNIHLdpMkasSeo1eri4uKrV68uXrzYMCU+efIkKSnp6NGjOOWflJR06dIlDMNsbGwsLS3pdLqTk5OPj8+SJUtwKlFfmK8RDxw4MHHiRBaLpXuy3TCIRKK7d++OHDkSp/wLCgoSExMbGhraX9RoNI6OjufPn8epUL1ggp2VnpCamtrc3Mzj8QzpQgAAm83Gz4UAAD8/P39//w4XWSwW5C40RyNeuXIFAPD6668vX77c8KXX19fv3LkT1yLmzJljY2PT9pZEIl2/fh3XEvWCeRlx48aNJSUlAAAHB2K2hhEKhVevXsW1iIiICC8vL13EpdFoPD09T58+jWuJeoG8bt06ojUYguLiYi6Xy2KxJk6cSKAMKpXq4uLi7u6OaylMJvP27dtyudzFxSU1NfXo0aMZGRnDhw/HtdBXxCw6K0lJSTExMaNHjyZaiOGYO3dubW3tH3/8oXubmpp68uTJX375hWhdXaM1aUQiUXl5+aVLl4gW8py6urodO3YQUnR+fn5YWFhubi4hpb8UU44RN2zY0NDQ4OLiEhsbS7SW5xggRuwKf3//rKysr7/++vhxvDaRehVM1oipqakDBgzAOxrrLXZ2dkuXLiVQwIEDB4qKij7//HMCNXSKCcaIycnJixYtUigUNNxm0oydM2fOHDx4MCUlBZ4/kanViJ999pm1tTUAAJ4/cXsMMI7YEyZPnvzll19GRUXl5OQQreV/EB2k6o2rV69qtdr6+nqihXRHcXHx9OnTiVbxNwsXLjx48CDRKrSm01mZO3eubtt+Pp9PtJbuIDxG7MDevXurq6s//ZT4HWaNPkasqKiws7MrKSnx8/MjWouxcvHixT179qSkpOjOgCEEI64RVSrV22+/LZPJaDSasbgQkhixA+PHj9+yZcv48ePv3LlDlAZjNaJWq83IyFiyZIm3tzfRWnoBgeOI3dOvX79r167t3bt3//79hAgwPiNqNJp///vfWq02KioqNDSUaDm9A7YYsQO7d+8WCASrVq0yfNHGFyOuXbs2JiZmxIgRRAsxWS5fvrx169aUlBTdQJiBILrb3gt+/vlnoiW8KgTONfeKysrK6OjoGzduGKxEo2max40bFxQURLSKVwXaGLEDTk5Oly9fPnLkyI8//miYEo2gac7Ozg4NDZXJZAZe1o8HeD+zond27dpVWFi4ZcsWvAuCukYUi8Vjx461srJqO7zT2MH7mRW9s2TJkoSEhLFjx9bV1eFbksGCgN4iEokKCwshn7LrLcYSI3agvr5+3LhxOTk5+BUBaY144sSJ7Ozs/v37Qz5l11sYDMa9e/eIVtFr+Hz+xYsXd+zYUVlZiVMRkB74U1RUpFQqiVahf9hs9s6dO6VSKYZhRhdsZGdnOznhda4RpDXiO++8M2nSJKJV4AKVSrWwsDhy5Eh1Nb6nP+uXgoICX19f3coSPIDUiBwOh8AJeAMwf/78xMQuz5GEkEePHr346L4egdSIP/zww7lz54hWgS9HjhwBAJSXlxMtpEfk5+cHBATglz+kRhQIBGKxmGgVhiA9Pf3u3btEq3g5eNeIkA5oCwQCCoVi2q1zG1988QUMS1O7Jzw8PCsrC7/8Ia0RTT5GbI/OhZmZmUQL6ZL8/Hxcq0N4jWgOMWIHKioqLl26RLSKzsG7XYbXiOYTI7Yxbdo0oVBItIrOwbunAq8RFy9ebKrjiN0wffp0AMChQ4eIFtIR860RzSpG7ACPx4NqVxCNRlNUVOTr64trKZAa0QxjxDZiY2Oh2inFAO0yvEY0wxixPeHh4bpdK4gWAgzTLsNrRPOMETuQkJBw8OBBolUYyIiQrr7hcDhESyCekJAQe3t7olWA/Pz82bNn410KpDWiOceI7dEtu0pISCBKgEqlevr0af/+/fEuCFIjmnmM2IHdu3enpKS0v2KwrUcN01NBc81Gg0KhUCgUZDLZwsJiwoQJtbW1Y8eO/eqrr/Au98iRI2VlZQZ45B7FiMYBjUaj0WjDhg2ztrauq6vDMCwvL6+pqYnL5eJabn5+fkREBK5F6IC0aUYxYqfweLyamhrd66amJgOc5GOYLjO8RkQx4ou88cYb7Z9dEovFaWlpuJaoUCjKy8u9vLxwLUUHpE3z4sWLKQY5t9ZYSEhIKCsr0x1pprtCIpHKyspKSko8PT1xKtRgPRV4a0RznmvulJMnTyYkJLi7u+s2RtJoNACA2tpaXFtng7XL8NaIP/zwg7OzM5pcac+aNWsAAA8ePLh+/fr169cbGxsFzZL0y7enTp6LU4mP856FhISImlV9zkGrBVbcHnkMruGb6OhogUDQJgnDMK1W6+DgcOHCBaKlwUVWWtODG80aTKWSay1wez5apVKRKZRXeYDUxpFeWSTxHsQaMoFnxaV2cydcNWJkZOSFCxfawiBdJBQXF0eoKOj4bX+NJZc6fqGbpXV3Py0kqJSaljrFse8qpr7rbGPX5ZkjcMWIs2fP7rCXgIuLiwEmOo2Iiz/X2DjQB43gGYULAQAUKonvzJjxgcfJHZXCpi5374DLiIGBge03QcQwbNy4cQbdtxRuSvPFNAtywFCbHtwLHaNmOmZeaOoqFS4jAgD++c9/tm285OLiMmPGDKIVQURduZxKh+4n6yE29vTiHFFXqdB9q4CAgIEDB+pejx8/3sbGKP/344RcouY70olW0UfIFMzNl9VSr+g0FTojAgAWLFjA4/EcHBxQddgBsVCtMuY90ppqFV1t4/SqveaqJxJBg0osUkmEao0aqFSaV8wQAAAAb5jvEhaLlXVRDkDtq2dHtyBhAGNakZlWZJ4T3dbJWCsVE6aPRix7JC7Mbi3JFds4WGi1GJlKJlHJJDJZX6OSQQNHAgBEepptbpVgGrVaXalSK2RKmUApU3sNZPmFs+37GdkOhSZMr41Y/VR67WQjlUnDKHSv12woVDI+wnBEIVU1NojTTzVbMMHweJ61LYwH6pobvTPiH4fqq0pkPA8uy8aI6xKaBYXrygEACOvEqdur/AezIyfxiBZl7vS0s6JSan5eXyZT091CnYzahe2xsmN5veZaV0M6uQOvraERPaRHRlSrtMlJJY4B9pY8E1wRY+1sReVYHd5sHBtmmiovN6JGo9216klAjAedZRxzSn3Akse0cubu/6KMaCHmy8uNePA/z/pHOhtEDJEwrRlcV+vze41pg3VT4iVGvJraYO1qTWeZRb+SbWepBPSc9BaihZgj3RmxsUr+NFfMtrU0oB6CsXbi3DjVANUaTTOhOyNeO9XI98D3aUUIcfCxuX6qkWgVZkeXRqwplarUJLYt07B6ekrOwz9WrhnSKm7We858d+vKErlcqtZ7zkZK/NTRB1JwPyy3SyMW3xdjZJPtJr8EjFSaJyFahH74fP3HFy6eJlrFy+nSiE8eiNl2kFaHeMPksopyWolWoR8eP84nWkKP6HyKr7lOYcGm4tdZLn324Pc/fyyvyLdk2fj7Dosd9RaDwQIAZGQeS0vft2ThrgOHk2rrShztvUdEzo4Iff4s37nftmfdv0CnMUMGjrXju+GkDQBgZceszoN0X/VeMSomHACwafOGXbu3nD19FQCQkZG+/0By2bOnHI61t7fv8vc+srd30N3cTVIbmbcyjhw5UPA4j8vlBwUNWvTWezyefo6P7bxGbG1RyaR6WdDVCQ2N5T/8/J5SKV+26Mf5c76uri3atW+JWq0CAJApVKlUdOr85hnxn2xanzkwKProqS+aW2oAADdvp968fXzqxA+XL/6JZ+OU9udenOTpHlFobVaKhX1/jBISfruQAQD4cOUanQuz7t76bN2HsbETjx6+sHbNxtra6q3bNuru7CapjcKigqRPloeERPy87/j776168qTw6/+u05fUzo0oEarJuC2ryb7/G4VMXTD7a3tbdwc7z+lTVldWP859lK5LVauVY0a91c91AIZh4cETtVptZXUhAODGX0cHBsYMDIpmMq0iQid5e4bjJE8HjUEWC4zeiB3Y99OuEcOjp70xh8OxDgwcuHTJB5mZNwoe53ef1EbuwxwGgzFv7kJ7e4chgyO/2bRr9uwF+tLWhRFFKjINrydNS589cHUJYLGePxLFtXHkcV2eluW03eDmHKh7wbSwAgBIZSKtVtvQVG5v59F2j4uTH07ydFAtyBLjrxE7UFJS5OcX2PbW1ycAAFBQkNd9UhtBA4JlMlnS6sRjxw9WVJZzONYhwXqrDrp0GwbwGtSVylrLK/NXrhnS/qJQ9PfQ3YuryWVysUajptP/7jzRaBY4ydOhUQOA29nEhNDa2iqXy+n0v1dOMZlMAIBEIu4mqX0OPv39Nv5n27Vrl5P3bN+5a0tY6OAF8xcHBQ3Si7zOjci0oqiVMr0U8CJsNs+jX/DY6EXtL7JY3W2IyKCzSCSysp0kuQLf4RW1Qs2ygmv3gVeEwWAAAGQyadsVsUQMAOBx+d0kdchkyODIIYMj31zwzt27t1JPHPpkdeLJE3+QyXqI4jpvmplsslqJ14iuk33/FkGNp3uIt2eY7p+lpY0dv7uTRTAMs7F2LH32sO3Ko8cZOMnToZCpmVbGt/i8GygUiq+Pf17eg7YruteeXv27SWqfQ07O3Vu3bwIA+HzbsWMnvbt0hahV1NBQrxd5nRvRikuh0vBqmEZEztZoNGcublEoZHX1Zecuff/N93Oqa4u7/9SgoNEP8//MefgHAODK9QNlFbk4ydOtfLO0pphAjUin021t7bKyMu/lZKlUqoT4mTcyrqamHhKKhPdysnbu+jY0JKK/ty8AoJukNnLz7q/7fNXZcydaWprzH+WeOHmYz7fl8231IrXzvzWHT1PJ1DKRgsHW/1Aik2m1ctmvf15P2bp7fl19qZtL4PT41S/tfIyOelMsbj514Ztfjq726Bc8eXzir8c+w2l1grBWbGNnIrNKc+cs/Onn3bfv3Dz067nY2In1DXVHjqV8v/Mbe3uH8LChb7+1THdbN0ltzJg+r6Wl+fsdm7/d8hWNRoseNXbLt8l6aZe72w3sr/ONFaVaW09zfL69Kq8uIsayfwibaCEd+W1/jZOXpccAY10PdXJ72ZR3nDj8Tv6TdznF5z2IpVWZ2vhFD8EwtUegCT4UATNdhkG2LgwLplZQK+bYd/6TtAjqNn/f+T5dFnRLqbzzuVoHW89li/b0VW0nfPplTFdJarWKTO7kC7q5BC6av62rT9WXNHsEWFBoMO6BYcJ0F4+PmMo/vrWyKyOyLbkfLE3pNEmhkNFonT/pRyLpuQfQlQYAgEIpp1E72dSBQuky8NWoNfVPBdPfNcT25Yj2dGcLDo/qP8SysV7Etu0kWiKTKVwbp84+Z1D0q0FYLRg5XT+z+Ihe8ZIGKHISX9LQKmnBa3AbKgTVQkuWJmAIOmuIAF4eCc38wOXZvRqlzMQ7Li01rdKm1tFz7IgWYqb0KCRf/LVnUUa5CdeLgppWIBPPWulKtBDzpUdGxDBs6WZvYWWTsLbLHT9gbvlfAAABr0lEQVSNl+byZhomjV9CfLxrzvRikGLWSlceT12SWSGsM5HDyZorhQVXyzx8KeMXdFyKjDAwvRtMeT2OFzCEfe1kY8MTiZZMtbJlGeM+JFKhXFQv0cjlfCfqhHX96BYmtbjBSOn1qJ6NHW3KYseaUllRTuuTB7V0JkWjwcg0MplKJlHIALdVjK8ChmEqpVqjUKkUaoVUSbcg9Q+29Am1RTsjwkMfh5cd3BkO7ozh8fymGoWgQSkWqsQClVqlUatgNCKNgZHIJJYVk2lF5jvTLDnGV4ubPK86z8F1oHEdUL2CeFXQjKoxweJQjHrTA64DvavgDRnRmLBgkRoq5USr6CNKhaaiUMzhd95+IiMaE/b9GEq5sW7K01Qj72aJJzKiMeHqw8QwcO+KUW5WduXXqtcnd7lpPlznNSN6wrUT9Uql1mugFc/JCHbVFwtVgnr5n4dr/rHajdX1eAUyolGS+5cg76ZQJlHLcdsZRi/YOtNb6hQeA1ivx/G7P84SGdGI0WqBQga1EbUaLYPVo4krZEQEFKDOCgIKkBERUICMiIACZEQEFCAjIqAAGREBBf8Hph49+fyMhM0AAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "3ed7c7ef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AI Message : The current weather in London, UK is 15Β°C with cloudy conditions.\n" + ] + } + ], + "source": [ + "weather_response = await graph.ainvoke({\"messages\": \"what is the weather in london?\"})\n", + "print(f\"AI Message : {weather_response['messages'][-1].content}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "backend", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/notebooks/reddit_data.json b/backend/notebooks/reddit_data.json new file mode 100644 index 0000000000000000000000000000000000000000..2c193afbef865b78c4f7e1ca4ca25430be115e91 --- /dev/null +++ b/backend/notebooks/reddit_data.json @@ -0,0 +1,3202 @@ +{ + "kind": "Listing", + "data": { + "after": "t3_1njq9kq", + "dist": 27, + "modhash": "", + "geo_filter": null, + "children": [ + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Rule 7 (Posts must be personal) still exists.\n\nNo, your hot takes about the election, whether celebratory or gloomy, are not what this subreddit is for.\n\nNo, you whining about how much you have to see posts about the election is not what this subreddit is for. Also, you're playing yourself when you do that.\n\nNo, making a post titled \"WWIII\" to bypass the filter (which includes both Trump and Harris) won't convince us to leave your post up.\n\nThere are many, many places to talk about the election on and off of reddit. This is not one of them. We've had dozens, possibly hundreds of posts removed. Given that nobody reads these pinned posts or the rules on the side, I expect we'll have dozens to hundreds more!\n\nComplaint section - Since this post will be locked.\n\n> \"This is censorship!\"\n\nSorry, you can't post pictures of muscle cars in /r/musclecats. This is about keeping the subreddit on topic.\n\n> \"You should just allow every post, ever!\"\n\nImagine if the OnlyFans bots could post and the mods weren't allowed to remove them.\n\n> \"Mods are just jannies!\"\n\nI don't approve of you insulting perfectly respectable sanitation workers by associating them with reddit moderators. Also, janitors get paid.\n\n> \"You don't understand, my hot take about the election is truly and deeply perso-\n\n*audible groaning*", + "author_fullname": "t2_6l6dj", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Reminder - We are not a political subreddit - Posts about the election will be removed.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1gl2ugq", + "quarantine": false, + "link_flair_text_color": "light", + "upvote_ratio": 0.77, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 209, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "RULE 7: POST MUST BE PERSONAL", + "can_mod_post": false, + "score": 209, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1730911533.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1730911454.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Rule 7 (Posts must be personal) still exists.</p>\n\n<p>No, your hot takes about the election, whether celebratory or gloomy, are not what this subreddit is for.</p>\n\n<p>No, you whining about how much you have to see posts about the election is not what this subreddit is for. Also, you&#39;re playing yourself when you do that.</p>\n\n<p>No, making a post titled &quot;WWIII&quot; to bypass the filter (which includes both Trump and Harris) won&#39;t convince us to leave your post up.</p>\n\n<p>There are many, many places to talk about the election on and off of reddit. This is not one of them. We&#39;ve had dozens, possibly hundreds of posts removed. Given that nobody reads these pinned posts or the rules on the side, I expect we&#39;ll have dozens to hundreds more!</p>\n\n<p>Complaint section - Since this post will be locked.</p>\n\n<blockquote>\n<p>&quot;This is censorship!&quot;</p>\n</blockquote>\n\n<p>Sorry, you can&#39;t post pictures of muscle cars in <a href=\"/r/musclecats\">/r/musclecats</a>. This is about keeping the subreddit on topic.</p>\n\n<blockquote>\n<p>&quot;You should just allow every post, ever!&quot;</p>\n</blockquote>\n\n<p>Imagine if the OnlyFans bots could post and the mods weren&#39;t allowed to remove them.</p>\n\n<blockquote>\n<p>&quot;Mods are just jannies!&quot;</p>\n</blockquote>\n\n<p>I don&#39;t approve of you insulting perfectly respectable sanitation workers by associating them with reddit moderators. Also, janitors get paid.</p>\n\n<blockquote>\n<p>&quot;You don&#39;t understand, my hot take about the election is truly and deeply perso-</p>\n</blockquote>\n\n<p><em>audible groaning</em></p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": true, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "ec665856-86d5-11eb-a295-0e6f39109baf", + "can_gild": false, + "spoiler": false, + "locked": true, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#ea0027", + "id": "1gl2ugq", + "is_robot_indexable": true, + "report_reasons": null, + "author": "TimPowerGamer", + "discussion_type": null, + "num_comments": 1, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1gl2ugq/reminder_we_are_not_a_political_subreddit_posts/", + "stickied": true, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1gl2ugq/reminder_we_are_not_a_political_subreddit_posts/", + "subreddit_subscribers": 2377691, + "created_utc": 1730911454.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "# Hello!\n\n\n\nAs the always lovely u/SuperBeavers1 pointed out in [this modpost](https://www.reddit.com/r/TrueOffMyChest/comments/1kylws6/note_from_moderators_regarding_ai/) earlier, our team is working hard on combatting AI. We do this by constantly updating our automoderator and by using several [devvit (apps for reddit)](https://developers.reddit.com/apps) tools such as bot-bouncer, evasion-guard, floodassistent and Read the Rules. \n\nThat last one, Read the Rules, seems to be a little bit confusing to people. So in this post we will briefly explain what it does and how to accept our rules via this Read the Rules app.\n\n\n\n**Why do we use this app?** \nRead The Rules is intended to help encourage users to actually read their community rules by requiring them to confirm that they have read them. This acknowledgement is available to us as mods to view and manage when carrying out their duties. So the \"*I didn't read the rules*\" argument is no longer valid. \n\nSo regardless if you are new to reddit or have been an avid visitor of our sub, your submission might get removed until you acknowledged our rules through this app. After accepting our rules, which is a one time only thing, you are good to go. \n\n*Keep in mind that after accepting the rules, your submission still can get held back for manual review because it triggers other filters.*\n\nWe hope that using this app will also lower the amount of bot/AI/karma farming accounts.\n\n \n**How does it work?** \nThe proces is basically the same for both PC and Mobile. But we will show you both!\n\n\n\n# For PC users:\n\n\n\n**1).** Go to r/TrueOffMyChest. \n\n**2).** Click the 3 dots on either the front page or any post or comment! \n*Yeah, you can even do it from this post.* \n\n\n\nhttps://preview.redd.it/1hpkbjpuj27f1.png?width=964&format=png&auto=webp&s=27d0cc1a2b230769fbf0db2a6d4b9835d284d862\n\n\n\n**3).** Click on **Read the Rules**.\n\n**4)**. A new menu will pop up that will take you through all of our rules. All rules are already selected, so you do not need to click any buttons. **Read them and scroll down.**\n\n\n\nhttps://preview.redd.it/1dawii72k27f1.png?width=951&format=png&auto=webp&s=9c2d5437388a78f1d0189917d21223648b40e4a0\n\n\n\n **5).** After reading our rules, you need acknowledge that you have read them and understand them. Y*es, now you need to switch that button!*\n\n\n\nhttps://preview.redd.it/cibor808k27f1.jpg?width=921&format=pjpg&auto=webp&s=866040d426ae602b74dfed1c388ca78c68bfc7a8\n\n**6).** After switching/clicking that button the colour will change. Now all you need to do is click on **Submit.**\n\n\n\n**And you are all set!**\n\n\n\n\\---\n\n\n\n# For mobile users:\n\n\n\n**1).** Go to r/TrueOffMyChest. \n\n**2).** Click the 3 dots on either the front page or any post or comment! \n*Yeah, you can even do it from this post.*\n\n \n\nhttps://preview.redd.it/2cmocnrek27f1.png?width=757&format=png&auto=webp&s=c6d6942ffc406070b2ac75d0dc46cd9bf47c3867\n\n\n\n**3).** Click on **Read the Rules.**\n\n**4)**. A new menu will pop up that will take you through all of our rules. All rules are already selected, so you do not need to click any buttons. **Read them and scroll down.**\n\n\n\nhttps://preview.redd.it/6lapwmnlk27f1.jpg?width=1290&format=pjpg&auto=webp&s=21de3e53cd4ac0334c097cd2c76a836d5b6c1927\n\n\n\n5). After reading our rules, you need acknowledge that you have read them and understand them. **Yes, now you need to switch that button!**\n\n\n\nhttps://preview.redd.it/jsbz9xxok27f1.jpg?width=770&format=pjpg&auto=webp&s=28becad494695c964aeac5ef2e223edb0e82e2d3\n\n\n\n**6).** After switching/clicking that button the colour will change. Now all you need to do is click on **Submit.**\n\n*Again, accepting the rules does not mean your post will automatically will be let through. We still have filters in place that can put your post in queue for manual review.*\n\n\n\n\\---\n\n\n\nhttps://i.redd.it/dloy4pp2m27f1.gif\n\n\n\n", + "author_fullname": "t2_z6qlu", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "How to: Read the Rules App", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "media_metadata": { + "6lapwmnlk27f1": { + "status": "valid", + "e": "Image", + "m": "image/jpg", + "p": [ + { + "y": 216, + "x": 108, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=108&crop=smart&auto=webp&s=c4a707333ec8c0beec103d1f807a4f672540e394" + }, + { + "y": 432, + "x": 216, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=216&crop=smart&auto=webp&s=d86a927a64d1a9f171858312485dd5bde9bf74f5" + }, + { + "y": 640, + "x": 320, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=320&crop=smart&auto=webp&s=e5f732be3f0ca34188a31ed2e8351f42fd11e395" + }, + { + "y": 1280, + "x": 640, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=640&crop=smart&auto=webp&s=07ae93858b718d61bcd6bd650e3ad1eb4b906deb" + }, + { + "y": 1920, + "x": 960, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=960&crop=smart&auto=webp&s=c1fe9bf12d5a5f88e7c63a82e0de3d821739230b" + }, + { + "y": 2160, + "x": 1080, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=1080&crop=smart&auto=webp&s=b18505e02b9a0fb1b13125eb71e45a01c114c66b" + } + ], + "s": { + "y": 2645, + "x": 1290, + "u": "https://preview.redd.it/6lapwmnlk27f1.jpg?width=1290&format=pjpg&auto=webp&s=21de3e53cd4ac0334c097cd2c76a836d5b6c1927" + }, + "id": "6lapwmnlk27f1" + }, + "1dawii72k27f1": { + "status": "valid", + "e": "Image", + "m": "image/png", + "p": [ + { + "y": 94, + "x": 108, + "u": "https://preview.redd.it/1dawii72k27f1.png?width=108&crop=smart&auto=webp&s=d263717d6d36476e2e704cbd424952596621cfbe" + }, + { + "y": 189, + "x": 216, + "u": "https://preview.redd.it/1dawii72k27f1.png?width=216&crop=smart&auto=webp&s=da519a24423a525def07d5c39e6082b1168b8302" + }, + { + "y": 280, + "x": 320, + "u": "https://preview.redd.it/1dawii72k27f1.png?width=320&crop=smart&auto=webp&s=feb9977f6a7581b8c66405ad6b8f7806681d769f" + }, + { + "y": 561, + "x": 640, + "u": "https://preview.redd.it/1dawii72k27f1.png?width=640&crop=smart&auto=webp&s=5938d6261c7ebbe2ccb5133da8792352a26f4677" + } + ], + "s": { + "y": 834, + "x": 951, + "u": "https://preview.redd.it/1dawii72k27f1.png?width=951&format=png&auto=webp&s=9c2d5437388a78f1d0189917d21223648b40e4a0" + }, + "id": "1dawii72k27f1" + }, + "jsbz9xxok27f1": { + "status": "valid", + "e": "Image", + "m": "image/jpg", + "p": [ + { + "y": 216, + "x": 108, + "u": "https://preview.redd.it/jsbz9xxok27f1.jpg?width=108&crop=smart&auto=webp&s=3d97f4f672f1d17931b5765dc9275d5205008855" + }, + { + "y": 432, + "x": 216, + "u": "https://preview.redd.it/jsbz9xxok27f1.jpg?width=216&crop=smart&auto=webp&s=69c2f0442e21c16dbf56ef8745744f789208a214" + }, + { + "y": 640, + "x": 320, + "u": "https://preview.redd.it/jsbz9xxok27f1.jpg?width=320&crop=smart&auto=webp&s=2e3db1ccda735e0f7d6c44c3382f6aa245a17e6c" + }, + { + "y": 1280, + "x": 640, + "u": "https://preview.redd.it/jsbz9xxok27f1.jpg?width=640&crop=smart&auto=webp&s=c6639f19633f545ce3e7ed03b13c86fc98fe0bfe" + } + ], + "s": { + "y": 1596, + "x": 770, + "u": "https://preview.redd.it/jsbz9xxok27f1.jpg?width=770&format=pjpg&auto=webp&s=28becad494695c964aeac5ef2e223edb0e82e2d3" + }, + "id": "jsbz9xxok27f1" + }, + "2cmocnrek27f1": { + "status": "valid", + "e": "Image", + "m": "image/png", + "p": [ + { + "y": 216, + "x": 108, + "u": "https://preview.redd.it/2cmocnrek27f1.png?width=108&crop=smart&auto=webp&s=f5fb9ddd4ba5a382c79b81b856abb19c47cd5fcc" + }, + { + "y": 432, + "x": 216, + "u": "https://preview.redd.it/2cmocnrek27f1.png?width=216&crop=smart&auto=webp&s=d27e7cb5efc572a3720bc243d702fcb6c973748f" + }, + { + "y": 640, + "x": 320, + "u": "https://preview.redd.it/2cmocnrek27f1.png?width=320&crop=smart&auto=webp&s=d9d6af993bc5eec51cf5efa4ff8a9238ccbedeef" + }, + { + "y": 1280, + "x": 640, + "u": "https://preview.redd.it/2cmocnrek27f1.png?width=640&crop=smart&auto=webp&s=0388167bda689d11a2d0c6bf08937d68296a4cd5" + } + ], + "s": { + "y": 1598, + "x": 757, + "u": "https://preview.redd.it/2cmocnrek27f1.png?width=757&format=png&auto=webp&s=c6d6942ffc406070b2ac75d0dc46cd9bf47c3867" + }, + "id": "2cmocnrek27f1" + }, + "dloy4pp2m27f1": { + "status": "valid", + "e": "AnimatedImage", + "m": "image/gif", + "p": [ + { + "y": 80, + "x": 108, + "u": "https://preview.redd.it/dloy4pp2m27f1.gif?width=108&crop=smart&format=png8&s=9ccad167acb67422467aba8c2c8b9c9a33e5e3f2" + }, + { + "y": 161, + "x": 216, + "u": "https://preview.redd.it/dloy4pp2m27f1.gif?width=216&crop=smart&format=png8&s=381548b5d3dbcd1bf849ef6fcff4707d9ce208cc" + }, + { + "y": 239, + "x": 320, + "u": "https://preview.redd.it/dloy4pp2m27f1.gif?width=320&crop=smart&format=png8&s=30e2cf9a26a28746b554b4479b0bee8fd2142865" + } + ], + "s": { + "y": 373, + "gif": "https://i.redd.it/dloy4pp2m27f1.gif", + "mp4": "https://preview.redd.it/dloy4pp2m27f1.gif?format=mp4&s=1f5fe839dcb514e33fdb74cf8a1df2a858cc3681", + "x": 498 + }, + "id": "dloy4pp2m27f1" + }, + "cibor808k27f1": { + "status": "valid", + "e": "Image", + "m": "image/jpg", + "p": [ + { + "y": 67, + "x": 108, + "u": "https://preview.redd.it/cibor808k27f1.jpg?width=108&crop=smart&auto=webp&s=e587bd2cef794e896b3a679c87b9dfd240e77749" + }, + { + "y": 134, + "x": 216, + "u": "https://preview.redd.it/cibor808k27f1.jpg?width=216&crop=smart&auto=webp&s=b75e7837ab6ad16af721a4246e3131667bbc127f" + }, + { + "y": 199, + "x": 320, + "u": "https://preview.redd.it/cibor808k27f1.jpg?width=320&crop=smart&auto=webp&s=4e278f192096d84902d7c66641ff60733fd82abc" + }, + { + "y": 398, + "x": 640, + "u": "https://preview.redd.it/cibor808k27f1.jpg?width=640&crop=smart&auto=webp&s=7ac375e04a05e438928934ddc93944dffbd4135c" + } + ], + "s": { + "y": 574, + "x": 921, + "u": "https://preview.redd.it/cibor808k27f1.jpg?width=921&format=pjpg&auto=webp&s=866040d426ae602b74dfed1c388ca78c68bfc7a8" + }, + "id": "cibor808k27f1" + }, + "1hpkbjpuj27f1": { + "status": "valid", + "e": "Image", + "m": "image/png", + "p": [ + { + "y": 65, + "x": 108, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=108&crop=smart&auto=webp&s=325f32bf30431949363555821e8d71ebd2d39e12" + }, + { + "y": 131, + "x": 216, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=216&crop=smart&auto=webp&s=b89d57327b4da67214bdd9c249f92e463b78fba8" + }, + { + "y": 195, + "x": 320, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=320&crop=smart&auto=webp&s=2f21f74ce38fd54d20dfa920334bf6a1b8f64569" + }, + { + "y": 390, + "x": 640, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=640&crop=smart&auto=webp&s=ad5a43d9274f82a90fb8fcb1da0dd6b047ad4d46" + }, + { + "y": 585, + "x": 960, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=960&crop=smart&auto=webp&s=933fa6b81d12dfba662e549d41b3f6cf2d564f5e" + } + ], + "s": { + "y": 588, + "x": 964, + "u": "https://preview.redd.it/1hpkbjpuj27f1.png?width=964&format=png&auto=webp&s=27d0cc1a2b230769fbf0db2a6d4b9835d284d862" + }, + "id": "1hpkbjpuj27f1" + } + }, + "name": "t3_1lbxglh", + "quarantine": false, + "link_flair_text_color": null, + "upvote_ratio": 0.99, + "author_flair_background_color": "#ad52a3", + "subreddit_type": "public", + "ups": 56, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": "fec5bc76-ff3a-11ef-9c0a-3e65d3123b9e", + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "Mod post", + "can_mod_post": false, + "score": 56, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1749984403.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><h1>Hello!</h1>\n\n<p>As the always lovely <a href=\"/u/SuperBeavers1\">u/SuperBeavers1</a> pointed out in <a href=\"https://www.reddit.com/r/TrueOffMyChest/comments/1kylws6/note_from_moderators_regarding_ai/\">this modpost</a> earlier, our team is working hard on combatting AI. We do this by constantly updating our automoderator and by using several <a href=\"https://developers.reddit.com/apps\">devvit (apps for reddit)</a> tools such as bot-bouncer, evasion-guard, floodassistent and Read the Rules. </p>\n\n<p>That last one, Read the Rules, seems to be a little bit confusing to people. So in this post we will briefly explain what it does and how to accept our rules via this Read the Rules app.</p>\n\n<p><strong>Why do we use this app?</strong><br/>\nRead The Rules is intended to help encourage users to actually read their community rules by requiring them to confirm that they have read them. This acknowledgement is available to us as mods to view and manage when carrying out their duties. So the &quot;<em>I didn&#39;t read the rules</em>&quot; argument is no longer valid. </p>\n\n<p>So regardless if you are new to reddit or have been an avid visitor of our sub, your submission might get removed until you acknowledged our rules through this app. After accepting our rules, which is a one time only thing, you are good to go. </p>\n\n<p><em>Keep in mind that after accepting the rules, your submission still can get held back for manual review because it triggers other filters.</em></p>\n\n<p>We hope that using this app will also lower the amount of bot/AI/karma farming accounts.</p>\n\n<p><strong>How does it work?</strong><br/>\nThe proces is basically the same for both PC and Mobile. But we will show you both!</p>\n\n<h1>For PC users:</h1>\n\n<p><strong>1).</strong> Go to <a href=\"/r/TrueOffMyChest\">r/TrueOffMyChest</a>. </p>\n\n<p><strong>2).</strong> Click the 3 dots on either the front page or any post or comment!<br/>\n<em>Yeah, you can even do it from this post.</em> </p>\n\n<p><a href=\"https://preview.redd.it/1hpkbjpuj27f1.png?width=964&amp;format=png&amp;auto=webp&amp;s=27d0cc1a2b230769fbf0db2a6d4b9835d284d862\">https://preview.redd.it/1hpkbjpuj27f1.png?width=964&amp;format=png&amp;auto=webp&amp;s=27d0cc1a2b230769fbf0db2a6d4b9835d284d862</a></p>\n\n<p><strong>3).</strong> Click on <strong>Read the Rules</strong>.</p>\n\n<p><strong>4)</strong>. A new menu will pop up that will take you through all of our rules. All rules are already selected, so you do not need to click any buttons. <strong>Read them and scroll down.</strong></p>\n\n<p><a href=\"https://preview.redd.it/1dawii72k27f1.png?width=951&amp;format=png&amp;auto=webp&amp;s=9c2d5437388a78f1d0189917d21223648b40e4a0\">https://preview.redd.it/1dawii72k27f1.png?width=951&amp;format=png&amp;auto=webp&amp;s=9c2d5437388a78f1d0189917d21223648b40e4a0</a></p>\n\n<p><strong>5).</strong> After reading our rules, you need acknowledge that you have read them and understand them. Y<em>es, now you need to switch that button!</em></p>\n\n<p><a href=\"https://preview.redd.it/cibor808k27f1.jpg?width=921&amp;format=pjpg&amp;auto=webp&amp;s=866040d426ae602b74dfed1c388ca78c68bfc7a8\">https://preview.redd.it/cibor808k27f1.jpg?width=921&amp;format=pjpg&amp;auto=webp&amp;s=866040d426ae602b74dfed1c388ca78c68bfc7a8</a></p>\n\n<p><strong>6).</strong> After switching/clicking that button the colour will change. Now all you need to do is click on <strong>Submit.</strong></p>\n\n<p><strong>And you are all set!</strong></p>\n\n<p>---</p>\n\n<h1>For mobile users:</h1>\n\n<p><strong>1).</strong> Go to <a href=\"/r/TrueOffMyChest\">r/TrueOffMyChest</a>. </p>\n\n<p><strong>2).</strong> Click the 3 dots on either the front page or any post or comment!<br/>\n<em>Yeah, you can even do it from this post.</em></p>\n\n<p><a href=\"https://preview.redd.it/2cmocnrek27f1.png?width=757&amp;format=png&amp;auto=webp&amp;s=c6d6942ffc406070b2ac75d0dc46cd9bf47c3867\">https://preview.redd.it/2cmocnrek27f1.png?width=757&amp;format=png&amp;auto=webp&amp;s=c6d6942ffc406070b2ac75d0dc46cd9bf47c3867</a></p>\n\n<p><strong>3).</strong> Click on <strong>Read the Rules.</strong></p>\n\n<p><strong>4)</strong>. A new menu will pop up that will take you through all of our rules. All rules are already selected, so you do not need to click any buttons. <strong>Read them and scroll down.</strong></p>\n\n<p><a href=\"https://preview.redd.it/6lapwmnlk27f1.jpg?width=1290&amp;format=pjpg&amp;auto=webp&amp;s=21de3e53cd4ac0334c097cd2c76a836d5b6c1927\">https://preview.redd.it/6lapwmnlk27f1.jpg?width=1290&amp;format=pjpg&amp;auto=webp&amp;s=21de3e53cd4ac0334c097cd2c76a836d5b6c1927</a></p>\n\n<p>5). After reading our rules, you need acknowledge that you have read them and understand them. <strong>Yes, now you need to switch that button!</strong></p>\n\n<p><a href=\"https://preview.redd.it/jsbz9xxok27f1.jpg?width=770&amp;format=pjpg&amp;auto=webp&amp;s=28becad494695c964aeac5ef2e223edb0e82e2d3\">https://preview.redd.it/jsbz9xxok27f1.jpg?width=770&amp;format=pjpg&amp;auto=webp&amp;s=28becad494695c964aeac5ef2e223edb0e82e2d3</a></p>\n\n<p><strong>6).</strong> After switching/clicking that button the colour will change. Now all you need to do is click on <strong>Submit.</strong></p>\n\n<p><em>Again, accepting the rules does not mean your post will automatically will be let through. We still have filters in place that can put your post in queue for manual review.</em></p>\n\n<p>---</p>\n\n<p><a href=\"https://i.redd.it/dloy4pp2m27f1.gif\">https://i.redd.it/dloy4pp2m27f1.gif</a></p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "2f4b2e40-49d6-11f0-a005-2e0a9465a39b", + "can_gild": false, + "spoiler": false, + "locked": true, + "author_flair_text": "Stepmod 🧹", + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": "moderator", + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#df20c6", + "id": "1lbxglh", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Raignbeau", + "discussion_type": null, + "num_comments": 3, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": "dark", + "permalink": "/r/TrueOffMyChest/comments/1lbxglh/how_to_read_the_rules_app/", + "stickied": true, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1lbxglh/how_to_read_the_rules_app/", + "subreddit_subscribers": 2377691, + "created_utc": 1749984403.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "So I’m 27m. I have a friend group from college with a few girls and guys 25-28\n\nThis one girl, who I considered one of my closest friends attempted suicide last week. I and the rest of the group realized we never did for her what she did for us.\n\nShe showed up at my house that night with these cookies she makes what I’m always telling her how much I love them. She hugged me and left, I knew it was off and ignored it. Should have trusted my gut and asked her to come in\n\nTurns out, she went to everyone’s house that night. She made everyone their favourite.\n\nShe was always doing nice stuff for everyone, and we all realized we never did it for her. If you needed a favour, a ride, anything she was the one. If you were sad and she knew it she was bringing dinner. None of us ever did that for her, I realized even when her mother passed away none of us went out of our way to do anything nice for her. I actually don’t think anyone checked on her after a few weeks\n\nThe worst part of all of this, one week before she asked all of us if we wanted to go for dinner and we were all busy. We realized it the other night at my house.. she texted everyone.\n\nI don’t even know why, she’s the sweetest and has always been the most caring person I’ve met. Never saw any of this coming because she was always smiling, always sweet. \n\n\nVery luckily she failed. When I get to see her again I’m going to change things", + "author_fullname": "t2_1y0qv1a1x8", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My friend attempted suicide and we have all realized we didn’t treat her as good as she treated us. Even the night of the attempt she thought about us and baked us all treats to leave :(", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nks3lp", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.96, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 935, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 935, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758250249.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>So I’m 27m. I have a friend group from college with a few girls and guys 25-28</p>\n\n<p>This one girl, who I considered one of my closest friends attempted suicide last week. I and the rest of the group realized we never did for her what she did for us.</p>\n\n<p>She showed up at my house that night with these cookies she makes what I’m always telling her how much I love them. She hugged me and left, I knew it was off and ignored it. Should have trusted my gut and asked her to come in</p>\n\n<p>Turns out, she went to everyone’s house that night. She made everyone their favourite.</p>\n\n<p>She was always doing nice stuff for everyone, and we all realized we never did it for her. If you needed a favour, a ride, anything she was the one. If you were sad and she knew it she was bringing dinner. None of us ever did that for her, I realized even when her mother passed away none of us went out of our way to do anything nice for her. I actually don’t think anyone checked on her after a few weeks</p>\n\n<p>The worst part of all of this, one week before she asked all of us if we wanted to go for dinner and we were all busy. We realized it the other night at my house.. she texted everyone.</p>\n\n<p>I don’t even know why, she’s the sweetest and has always been the most caring person I’ve met. Never saw any of this coming because she was always smiling, always sweet. </p>\n\n<p>Very luckily she failed. When I get to see her again I’m going to change things</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nks3lp", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Throwra1793743873", + "discussion_type": null, + "num_comments": 69, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nks3lp/my_friend_attempted_suicide_and_we_have_all/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nks3lp/my_friend_attempted_suicide_and_we_have_all/", + "subreddit_subscribers": 2377691, + "created_utc": 1758250249.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I am 28 and I found out I was pregnant 5 months ago after me and my husband have struggled with infertility for 7 years we don’t have the money for more IVF so we’ve just been hoping and praying and I guess it finally worked.\n\nBut while at my pre natal appointment the doctor picked up on an abnormality and later diagnosed my son with a fatal birth defect called anencephaly. My baby has a brain stem but his brain is basically non existent. He will never be conscious, never be able to feel anything, he won’t even know he’s alive. And there’s nothing anyone can do.\n\nI’m a good person, I know that so why does the universe keep giving me these horrible experiences. And my baby, he hasn’t lived, he hasn’t ever done anything wrong and he’s going to die. Me and my husband are little more than robots at the moment and I just want to talk about it to people who won’t start crying at me.", + "author_fullname": "t2_1y2eav6hwf", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I’m pregnant and my baby is going to die", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkftpy", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.98, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 3288, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "CONTENT WARNING: VIOLENCE/DEATH", + "can_mod_post": false, + "score": 3288, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758219170.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I am 28 and I found out I was pregnant 5 months ago after me and my husband have struggled with infertility for 7 years we don’t have the money for more IVF so we’ve just been hoping and praying and I guess it finally worked.</p>\n\n<p>But while at my pre natal appointment the doctor picked up on an abnormality and later diagnosed my son with a fatal birth defect called anencephaly. My baby has a brain stem but his brain is basically non existent. He will never be conscious, never be able to feel anything, he won’t even know he’s alive. And there’s nothing anyone can do.</p>\n\n<p>I’m a good person, I know that so why does the universe keep giving me these horrible experiences. And my baby, he hasn’t lived, he hasn’t ever done anything wrong and he’s going to die. Me and my husband are little more than robots at the moment and I just want to talk about it to people who won’t start crying at me.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "9ec420a4-c82b-11ec-99b0-427d70aa494a", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#ffd635", + "id": "1nkftpy", + "is_robot_indexable": true, + "report_reasons": null, + "author": "BeingFriendly3383", + "discussion_type": null, + "num_comments": 177, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkftpy/im_pregnant_and_my_baby_is_going_to_die/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkftpy/im_pregnant_and_my_baby_is_going_to_die/", + "subreddit_subscribers": 2377691, + "created_utc": 1758219170.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "when I was a kid, I was diagnosed with autism, then when I was 12, I was diagnosed with depression and BPD, it was tough life, I somehow passed high school and tried to go to university many times but failed miserably\n\nit's a long process, I was in therapy for so long, I wasted so much money, switched so many therapists and medications, my therapists gave up\n\nI applied to assisted suicide when I was 19, im 22 now and I got a call from the doctor today, in 4-6 months, ill be dying and im so happy", + "author_fullname": "t2_1xbojuliuz", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I got approved for assisted suicide", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkoa2l", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.75, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 730, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 730, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758239464.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>when I was a kid, I was diagnosed with autism, then when I was 12, I was diagnosed with depression and BPD, it was tough life, I somehow passed high school and tried to go to university many times but failed miserably</p>\n\n<p>it&#39;s a long process, I was in therapy for so long, I wasted so much money, switched so many therapists and medications, my therapists gave up</p>\n\n<p>I applied to assisted suicide when I was 19, im 22 now and I got a call from the doctor today, in 4-6 months, ill be dying and im so happy</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkoa2l", + "is_robot_indexable": true, + "report_reasons": null, + "author": "ihatemylife56986", + "discussion_type": null, + "num_comments": 219, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkoa2l/i_got_approved_for_assisted_suicide/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkoa2l/i_got_approved_for_assisted_suicide/", + "subreddit_subscribers": 2377691, + "created_utc": 1758239464.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I have XX chromosomes . I menstruate monthly and my period is 5 days . \n\nHowever I look nothing like a woman . I have 0 female secondary sexual characteristics . It’s as if I went through male puberty because I physically resemble a man vs a woman . \n\n\n\n-My voice is very deep even deeper than many man . On the phone and irl I get gendered sir . \n\n\n\n-I have broad ribcage and flat chested despite being thin my band size is a 42 AAA \n\n\n-I have massive hands and feet . Woman’s gloves , rings , bracelets and shoes are way too small . I have to shop in the male section since I have size 12 men’s feet . \n\n\n-I am very hairy grow hair on my chest , stomach , and face . \n\n\n\n-I am balding with very thin hair . \n\n\n\n-0 curves inverted triangle body shape with broad shoulders and narrow hips .\n\n\n-Android narrow pelvis \n\n\n\n\n-Adam’s apple \n\n\n-Way bigger than most woman . I am big boned with a very muscular body . I gain muscle mass very quickly . I have huge organs . I had a CT scan done and even doctor commented that I have very big lungs and heart . \n\n\n\n", + "user_reports": [], + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Cis woman here who looks identical to a man", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nksiy2", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.91, + "author_flair_background_color": "", + "subreddit_type": "public", + "ups": 229, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 229, + "approved_by": null, + "is_created_from_ads_ui": false, + "thumbnail": "self", + "edited": 1758252131.0, + "author_flair_css_class": null, + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758251497.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I have XX chromosomes . I menstruate monthly and my period is 5 days . </p>\n\n<p>However I look nothing like a woman . I have 0 female secondary sexual characteristics . It’s as if I went through male puberty because I physically resemble a man vs a woman . </p>\n\n<p>-My voice is very deep even deeper than many man . On the phone and irl I get gendered sir . </p>\n\n<p>-I have broad ribcage and flat chested despite being thin my band size is a 42 AAA </p>\n\n<p>-I have massive hands and feet . Woman’s gloves , rings , bracelets and shoes are way too small . I have to shop in the male section since I have size 12 men’s feet . </p>\n\n<p>-I am very hairy grow hair on my chest , stomach , and face . </p>\n\n<p>-I am balding with very thin hair . </p>\n\n<p>-0 curves inverted triangle body shape with broad shoulders and narrow hips .</p>\n\n<p>-Android narrow pelvis </p>\n\n<p>-Adam’s apple </p>\n\n<p>-Way bigger than most woman . I am big boned with a very muscular body . I gain muscle mass very quickly . I have huge organs . I had a CT scan done and even doctor commented that I have very big lungs and heart . </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nksiy2", + "is_robot_indexable": true, + "report_reasons": null, + "author": "[deleted]", + "discussion_type": null, + "num_comments": 56, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_flair_text_color": "dark", + "permalink": "/r/TrueOffMyChest/comments/1nksiy2/cis_woman_here_who_looks_identical_to_a_man/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nksiy2/cis_woman_here_who_looks_identical_to_a_man/", + "subreddit_subscribers": 2377691, + "created_utc": 1758251497.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I’m not looking for sympathy, I’m not justifying anything. I fucked up and its my fault. I just need to get this out because there’s no one I can talk to.\n\nMy wife and I were having issues. Just the usual issues the struggle and strain of life, raising a family etc. We were struggling and nothing was getting better and I felt like I wasn’t good enough. That I never would be.\n\nAnd then I met β€˜Carly’ online. She was much younger than me so we just talked but then she started flirting with me and it made me feel good. I didn’t tell her I was married, didn’t want her to stop flirting.\n\nI told myself it wouldn’t go anywhere. I was just enjoying the attention. And we were just talking. She lived the other side of the world there was no chance of us meeting. And then we had cybersex. I felt better than I had in ages. Cybersex then became video and phone sex anytime my wife was out. We sent photos and videos every day.\n\nThe more I spent time with Carly, the more I couldn’t stand being with my wife so I broke up with her.\n\nI didn’t tell my wife about the affair, I gave other excuses but my wife knew something was up and found out about the affair.\n\nIt broke her. She didn’t eat, didn’t sleep, she cried all the time. I justified it by telling myself my wife is a strong woman she’ll get over it. I hate myself for thinking that way. But I did.\n\nMy wife went to therapy. Stopped crying. Started eating and sleeping again. Started smiling again. Stopped begging me not to leave. And I thought great. See I was right. I stopped feeling guilty. I felt relieved.\n\nMy wife and I had to live together for a while until I found a place but I barely saw her and she barely spoke to me. At first it was great but then I started to feel off, like I had come home to an empty house, even though it wasn’t.\n\nAt that point I should have seen sense, should have stopped. Instead I started to resent my wife. Somehow in my mind she was trying to sabotage my happiness. It made me angry. I snapped. Made passive aggressive comments – I hate myself for every word, every nasty text. Every accusation.\n\nI moved out.\n\nLiving with my wife had been awkward but the new place was…. I don’t know. Even though I’d rarely see her, every room contained her presence even when she wasn’t there. But staying in the new place made me feel more alone than I ever had. I had free run to talk to Carly any time I wanted, to do anything I wanted but it felt so pointless. The new place felt so fucking awful. Like a prison.\n\nI started to dread going home. I’d stay out for hours. Hang around supermarkets. Wander the streets. Sit on a park bench. Anything but go home. Even if it meant not talking to Carly.\n\nAnd then one time I passed a perfume shop and smelled my wife’s perfume and I don’t know why but I broke down. In that moment I didn’t want to talk to Carly. I wanted my wife.\n\nCarly and I broke up. I thought I’d miss her. I didn’t. I missed things my wife did. Small things. Big things. I didn’t miss a single thing Carly did.\n\nDuring handover of our daughter one day I blurted out that Carly and I broke up. I don’t know why, I didn’t even mean to, it just came out. My wife nodded and said I’m sorry to hear that. And I don’t know why but that stung. She didn’t say it spitefully, she was calm and pleasant, like we were just talking about the weather or something. I almost wish she did say it with some spite or glee or something. But she didn’t.\n\nAny time I try to talk about us or what happened, my wife shuts the conversation down.\n\nShe’s civil but she looks at me like I’m a stranger. The other day, I put my hand on her back just out of habit and she looked so…. so disgusted. I’ve never seen her make that face and certainly not at me.\n\nI feel so fucking broken. And I know its all my fault. I know I did this. I deserve all of this.\n\nI sabotaged everything good in my life. For nothing. For a lie. Carly didn’t know I was married and nobody knew I was even seeing anyone else even months after the separation. What was I doing???\n\nI got served divorce papers this morning.\n\nI’m not looking for sympathy. I don’t deserve it. I know I’m a selfish stupid prick. I know its all my fault.\n\nI wish I could go back but I can’t. And the worst part is I don’t even know why I did it. Yeah we had problems but I can think of a thousand ways to fix them now, why didn’t I think of them then?\n\nI’m sitting here staring at the divorce papers. And I don’t know what to do. My first instinct was to fight them. But I can’t. I shouldn’t. I want to fight it so bad hurts but I can’t. Not after what I did.\n\nI ended up calling in sick and I’ve been sitting at the kitchen counter, crying, thinking about everything I did, everything I said, wishing I could take it all back.\n\nThere’s no one I can talk to about this. The person I’d normally talk to is my wife, but I fucked that up.\n\nEveryone hates me. My friends. My family. Its deserved hate. I deserve all of this. I did it to myself, to everyone. I just wanted to get it off my chest, because I don’t know what else to do or where else to turn. Guess internet strangers are my only option.", + "author_fullname": "t2_1x8t5ggu8m", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I put a grenade in my relationship with my wife, I lost everything, and have nobody to blame but myself. I just need to get this out.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nk7qwo", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.81, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 3191, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 3191, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758200699.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I’m not looking for sympathy, I’m not justifying anything. I fucked up and its my fault. I just need to get this out because there’s no one I can talk to.</p>\n\n<p>My wife and I were having issues. Just the usual issues the struggle and strain of life, raising a family etc. We were struggling and nothing was getting better and I felt like I wasn’t good enough. That I never would be.</p>\n\n<p>And then I met β€˜Carly’ online. She was much younger than me so we just talked but then she started flirting with me and it made me feel good. I didn’t tell her I was married, didn’t want her to stop flirting.</p>\n\n<p>I told myself it wouldn’t go anywhere. I was just enjoying the attention. And we were just talking. She lived the other side of the world there was no chance of us meeting. And then we had cybersex. I felt better than I had in ages. Cybersex then became video and phone sex anytime my wife was out. We sent photos and videos every day.</p>\n\n<p>The more I spent time with Carly, the more I couldn’t stand being with my wife so I broke up with her.</p>\n\n<p>I didn’t tell my wife about the affair, I gave other excuses but my wife knew something was up and found out about the affair.</p>\n\n<p>It broke her. She didn’t eat, didn’t sleep, she cried all the time. I justified it by telling myself my wife is a strong woman she’ll get over it. I hate myself for thinking that way. But I did.</p>\n\n<p>My wife went to therapy. Stopped crying. Started eating and sleeping again. Started smiling again. Stopped begging me not to leave. And I thought great. See I was right. I stopped feeling guilty. I felt relieved.</p>\n\n<p>My wife and I had to live together for a while until I found a place but I barely saw her and she barely spoke to me. At first it was great but then I started to feel off, like I had come home to an empty house, even though it wasn’t.</p>\n\n<p>At that point I should have seen sense, should have stopped. Instead I started to resent my wife. Somehow in my mind she was trying to sabotage my happiness. It made me angry. I snapped. Made passive aggressive comments – I hate myself for every word, every nasty text. Every accusation.</p>\n\n<p>I moved out.</p>\n\n<p>Living with my wife had been awkward but the new place was…. I don’t know. Even though I’d rarely see her, every room contained her presence even when she wasn’t there. But staying in the new place made me feel more alone than I ever had. I had free run to talk to Carly any time I wanted, to do anything I wanted but it felt so pointless. The new place felt so fucking awful. Like a prison.</p>\n\n<p>I started to dread going home. I’d stay out for hours. Hang around supermarkets. Wander the streets. Sit on a park bench. Anything but go home. Even if it meant not talking to Carly.</p>\n\n<p>And then one time I passed a perfume shop and smelled my wife’s perfume and I don’t know why but I broke down. In that moment I didn’t want to talk to Carly. I wanted my wife.</p>\n\n<p>Carly and I broke up. I thought I’d miss her. I didn’t. I missed things my wife did. Small things. Big things. I didn’t miss a single thing Carly did.</p>\n\n<p>During handover of our daughter one day I blurted out that Carly and I broke up. I don’t know why, I didn’t even mean to, it just came out. My wife nodded and said I’m sorry to hear that. And I don’t know why but that stung. She didn’t say it spitefully, she was calm and pleasant, like we were just talking about the weather or something. I almost wish she did say it with some spite or glee or something. But she didn’t.</p>\n\n<p>Any time I try to talk about us or what happened, my wife shuts the conversation down.</p>\n\n<p>She’s civil but she looks at me like I’m a stranger. The other day, I put my hand on her back just out of habit and she looked so…. so disgusted. I’ve never seen her make that face and certainly not at me.</p>\n\n<p>I feel so fucking broken. And I know its all my fault. I know I did this. I deserve all of this.</p>\n\n<p>I sabotaged everything good in my life. For nothing. For a lie. Carly didn’t know I was married and nobody knew I was even seeing anyone else even months after the separation. What was I doing???</p>\n\n<p>I got served divorce papers this morning.</p>\n\n<p>I’m not looking for sympathy. I don’t deserve it. I know I’m a selfish stupid prick. I know its all my fault.</p>\n\n<p>I wish I could go back but I can’t. And the worst part is I don’t even know why I did it. Yeah we had problems but I can think of a thousand ways to fix them now, why didn’t I think of them then?</p>\n\n<p>I’m sitting here staring at the divorce papers. And I don’t know what to do. My first instinct was to fight them. But I can’t. I shouldn’t. I want to fight it so bad hurts but I can’t. Not after what I did.</p>\n\n<p>I ended up calling in sick and I’ve been sitting at the kitchen counter, crying, thinking about everything I did, everything I said, wishing I could take it all back.</p>\n\n<p>There’s no one I can talk to about this. The person I’d normally talk to is my wife, but I fucked that up.</p>\n\n<p>Everyone hates me. My friends. My family. Its deserved hate. I deserve all of this. I did it to myself, to everyone. I just wanted to get it off my chest, because I don’t know what else to do or where else to turn. Guess internet strangers are my only option.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nk7qwo", + "is_robot_indexable": true, + "report_reasons": null, + "author": "ThrowRA_Over_Volume", + "discussion_type": null, + "num_comments": 988, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nk7qwo/i_put_a_grenade_in_my_relationship_with_my_wife_i/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nk7qwo/i_put_a_grenade_in_my_relationship_with_my_wife_i/", + "subreddit_subscribers": 2377691, + "created_utc": 1758200699.0, + "num_crossposts": 6, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Hey all! This is an alt account because my parents follow my main. \n\nThis all started about 10 years ago! \n\nI (36F) downloaded an app off of the Playstore that promised to give you cash if you watched ads. To my shock, it actually did! And I started winning big when I gambled my points from watching ads to the point where I had about 14 million points. You could use the points for gift cards. So I started grabbing $25 Starbucks cards. Before you ask, the app doesn't exist in the same format any more. :( All good things must come to an end. Anyway! 14 million points translates to about $700 worth of gift cards. But the catch was that they only restocked cards like 3 times per day, and it was first come, first serve. It took about 2 years of everyday ads to get money like that, but I stuck with it. The rewards were sweet. I claimed many $25 Starbucks cards! And yes, it's really was valid. It was awesome. Was. Eventually, the restocks got few and far between, and then just stopped. But to be fair, I had it real good for like 6 years. \n\nNow comes the secret. I always used those gift cards to treat my parents to Starbucks. My mom (67F) and my dad (73M). I told them about the app and how I had an insane amount of cash on there, and we were able to get Starbucks basically once a week for many years. All it took was about an hour of ads every day. Sweet deal. It was nice to give my parents something. We were never a rich family, and they took care of me. But, as I said... The app stopped being that awesome. Eventually, my points were useless because they stopped restocking. However, I enjoyed how happy it made my parents and how they'd light up when I brought them their favourite orders. The time we've spent just having a little lunch all together is precious to me. So even though I was no longer getting gift cards, I decided to not tell them that the app closed down. Because I know that if I ever told them I was paying for all of it, they would refuse because they know I barely scrape by. They only allow me to treat them so frequently because it's supposed to be free.\n\nThey continue to brag about how I get the gift cards. Every single time, they laugh and smile and are so excited that they get free Starbucks. When they call or we just talk, they always ask if I've watched my ads for today yet. I always tell them of course! My dad loves to know how many points I have now. Which is 0 because I uninstalled the app, but he doesn't need to know! They both thank me all the time and it's a little slice of joy once a month, or sometimes once a week. \n\nI am never going to tell them that I have been paying for it for about 5 years now. I have no plans to stop. I still buy them Starbucks every time I see them, or we are out for errands or something. This secret will go to the grave with me. :) I just wanted to tell someone without it getting back to them. Today, I surprised them with lunch because they're going through a hard time, so it's fresh in my mind, and I had to make a post! It will always bring a smile to my face. I'm the type who never lies if I can help it, so I always get that OCD itch that I'm lying, but giving them Starbucks makes us so happy. It's cute that it's such a point of excitement for them, and I always want it to be that way. β™‘\n\nThanks for reading my little secret. Don't tell anyone! ;P \n\nTL;DR: \nAd app gave me tons of $25 Starbucks gift cards. Treated my parents to Starbucks for years once a weekish, they always got so excited it was free and still do, except it's not free any more. My secret is that the app is long gone, and I've been paying for it for about 5 years now. My parents have so much fun, asking me if I've watched my ads today. They light up when I drop by and surprise them. I know they'd refuse if they knew I was paying, so I am never going to tell them. Just so that they still have their joy about it.", + "author_fullname": "t2_44j0ehk8", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I Have Been Keeping A Secret From My Parents For Years!", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkpb3c", + "quarantine": false, + "link_flair_text_color": "light", + "upvote_ratio": 0.97, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 200, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "Positive", + "can_mod_post": false, + "score": 200, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758242328.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Hey all! This is an alt account because my parents follow my main. </p>\n\n<p>This all started about 10 years ago! </p>\n\n<p>I (36F) downloaded an app off of the Playstore that promised to give you cash if you watched ads. To my shock, it actually did! And I started winning big when I gambled my points from watching ads to the point where I had about 14 million points. You could use the points for gift cards. So I started grabbing $25 Starbucks cards. Before you ask, the app doesn&#39;t exist in the same format any more. :( All good things must come to an end. Anyway! 14 million points translates to about $700 worth of gift cards. But the catch was that they only restocked cards like 3 times per day, and it was first come, first serve. It took about 2 years of everyday ads to get money like that, but I stuck with it. The rewards were sweet. I claimed many $25 Starbucks cards! And yes, it&#39;s really was valid. It was awesome. Was. Eventually, the restocks got few and far between, and then just stopped. But to be fair, I had it real good for like 6 years. </p>\n\n<p>Now comes the secret. I always used those gift cards to treat my parents to Starbucks. My mom (67F) and my dad (73M). I told them about the app and how I had an insane amount of cash on there, and we were able to get Starbucks basically once a week for many years. All it took was about an hour of ads every day. Sweet deal. It was nice to give my parents something. We were never a rich family, and they took care of me. But, as I said... The app stopped being that awesome. Eventually, my points were useless because they stopped restocking. However, I enjoyed how happy it made my parents and how they&#39;d light up when I brought them their favourite orders. The time we&#39;ve spent just having a little lunch all together is precious to me. So even though I was no longer getting gift cards, I decided to not tell them that the app closed down. Because I know that if I ever told them I was paying for all of it, they would refuse because they know I barely scrape by. They only allow me to treat them so frequently because it&#39;s supposed to be free.</p>\n\n<p>They continue to brag about how I get the gift cards. Every single time, they laugh and smile and are so excited that they get free Starbucks. When they call or we just talk, they always ask if I&#39;ve watched my ads for today yet. I always tell them of course! My dad loves to know how many points I have now. Which is 0 because I uninstalled the app, but he doesn&#39;t need to know! They both thank me all the time and it&#39;s a little slice of joy once a month, or sometimes once a week. </p>\n\n<p>I am never going to tell them that I have been paying for it for about 5 years now. I have no plans to stop. I still buy them Starbucks every time I see them, or we are out for errands or something. This secret will go to the grave with me. :) I just wanted to tell someone without it getting back to them. Today, I surprised them with lunch because they&#39;re going through a hard time, so it&#39;s fresh in my mind, and I had to make a post! It will always bring a smile to my face. I&#39;m the type who never lies if I can help it, so I always get that OCD itch that I&#39;m lying, but giving them Starbucks makes us so happy. It&#39;s cute that it&#39;s such a point of excitement for them, and I always want it to be that way. β™‘</p>\n\n<p>Thanks for reading my little secret. Don&#39;t tell anyone! ;P </p>\n\n<p>TL;DR: \nAd app gave me tons of $25 Starbucks gift cards. Treated my parents to Starbucks for years once a weekish, they always got so excited it was free and still do, except it&#39;s not free any more. My secret is that the app is long gone, and I&#39;ve been paying for it for about 5 years now. My parents have so much fun, asking me if I&#39;ve watched my ads today. They light up when I drop by and surprise them. I know they&#39;d refuse if they knew I was paying, so I am never going to tell them. Just so that they still have their joy about it.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "d386b21c-a7ad-11ee-b535-6eb436ee5de3", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#94e044", + "id": "1nkpb3c", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Ok_Ad1285", + "discussion_type": null, + "num_comments": 5, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkpb3c/i_have_been_keeping_a_secret_from_my_parents_for/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkpb3c/i_have_been_keeping_a_secret_from_my_parents_for/", + "subreddit_subscribers": 2377691, + "created_utc": 1758242328.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Hi everyone. \n\nFirst off, thanks for everyone for their supportive comments, especially Bajanbeautykatie for the email template. That was very nice, although I did start of by sending something less confrontational. \n\nTo answer the most common questions: \n\nThe school had documentation to call my husband, or his mother ever since we enrolled there. I double checked our computer portal with the school website and it's still listed that way, including that I can't be contacted for anything that might be time sensitive. \n\nI cannot have my phone on my person while I'm working, period. \n\nMy work place has an automatic answering machine for public calls, so even if the school did call them I wouldn't get the message for probably another half hour at absolute best. Even then, I work about 30-40 minutes away if traffic is good. \n\nYes, I am in a more traditional area, although its never been too huge of a deal before besides having to commute to the city for work. \n\nThis is not going to be the super dramatic update I'm sure a lot of people were hoping for. Sorry? \n\nFirst off, I did not jump straight to getting an attorney to threaten them. I did call and ask a local family law firm and the person I spoke to told me if we did have to go as far as suing it would look better to try to exhaust options on my own before threatening legal action, but they would be happy to look over any communications between us and we could CC them on any emails and asked me to get any information on the potential neglect/abandonment case I could while they looked into it as well. \n\nI started by sending a follow up email to the principal, and CC'd the superintendent and LawPerson on it asking for confirmation that they had checked our file for who to call, more details on who exactly was spoken to at CPS, any case numbers, and the name of the person who was sitting alone with my sick daughter and did not speak to my husband or identify themselves. Unfortunately(or maybe fortunately?) the principal was out of town for several days with some family emergency. \n\nAfter a day with no reply the superintendent emailed me directly asking for more details, and I sent them an email outlining exactly what had happened from our perspective, screen shots from my phone, my husband's phone, and his mother's phone showing the phone calls and the lack of them. \n\nMonday the principal finally got back to us and we got some answers. \n\nThe woman sitting with our daughter was one of the school councilors, just not the one assigned to her. \n\nNo one actually contacted CPS, there is no case open against us, that was just a straight up lie. The woman who told me she had, had actually called the schools social worker(not CPS), who then sent the counselor to sit with her. Instead of, you know, telling her that was ridiculous or going himself. The counselor claims she was under the impression that she was just keeping our daughter company until the parents arrived, since there was no nurse that day. But if that was the case she should have at least said hello, right? \n\nAnd I'm not sure if he was supposed to tell me this, but apparently this is not the first time they've had issues with how she responds to fathers or male care givers in general. Which I want to know, if that’s the case why didn’t anyone do anything about it before? What the fuck? \n\nAs of now she's been suspended pending investigation. \n\nObviously these aren't all of the details, but this is the gist of it. \n\nI'm sure a lot of people were hoping to hear I'd sued the school for defamation, harassment, threatening, whatever else and gotten that stupid woman fired for being a misogynistic bitch. \n\nBut, this is what we've got lol. ", + "author_fullname": "t2_1i4s16edak", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "UPDATE : I think my kids school lied about calling CPS rather than calling my husband to pick her up", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nk3dca", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.99, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 3238, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 3238, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758186658.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Hi everyone. </p>\n\n<p>First off, thanks for everyone for their supportive comments, especially Bajanbeautykatie for the email template. That was very nice, although I did start of by sending something less confrontational. </p>\n\n<p>To answer the most common questions: </p>\n\n<p>The school had documentation to call my husband, or his mother ever since we enrolled there. I double checked our computer portal with the school website and it&#39;s still listed that way, including that I can&#39;t be contacted for anything that might be time sensitive. </p>\n\n<p>I cannot have my phone on my person while I&#39;m working, period. </p>\n\n<p>My work place has an automatic answering machine for public calls, so even if the school did call them I wouldn&#39;t get the message for probably another half hour at absolute best. Even then, I work about 30-40 minutes away if traffic is good. </p>\n\n<p>Yes, I am in a more traditional area, although its never been too huge of a deal before besides having to commute to the city for work. </p>\n\n<p>This is not going to be the super dramatic update I&#39;m sure a lot of people were hoping for. Sorry? </p>\n\n<p>First off, I did not jump straight to getting an attorney to threaten them. I did call and ask a local family law firm and the person I spoke to told me if we did have to go as far as suing it would look better to try to exhaust options on my own before threatening legal action, but they would be happy to look over any communications between us and we could CC them on any emails and asked me to get any information on the potential neglect/abandonment case I could while they looked into it as well. </p>\n\n<p>I started by sending a follow up email to the principal, and CC&#39;d the superintendent and LawPerson on it asking for confirmation that they had checked our file for who to call, more details on who exactly was spoken to at CPS, any case numbers, and the name of the person who was sitting alone with my sick daughter and did not speak to my husband or identify themselves. Unfortunately(or maybe fortunately?) the principal was out of town for several days with some family emergency. </p>\n\n<p>After a day with no reply the superintendent emailed me directly asking for more details, and I sent them an email outlining exactly what had happened from our perspective, screen shots from my phone, my husband&#39;s phone, and his mother&#39;s phone showing the phone calls and the lack of them. </p>\n\n<p>Monday the principal finally got back to us and we got some answers. </p>\n\n<p>The woman sitting with our daughter was one of the school councilors, just not the one assigned to her. </p>\n\n<p>No one actually contacted CPS, there is no case open against us, that was just a straight up lie. The woman who told me she had, had actually called the schools social worker(not CPS), who then sent the counselor to sit with her. Instead of, you know, telling her that was ridiculous or going himself. The counselor claims she was under the impression that she was just keeping our daughter company until the parents arrived, since there was no nurse that day. But if that was the case she should have at least said hello, right? </p>\n\n<p>And I&#39;m not sure if he was supposed to tell me this, but apparently this is not the first time they&#39;ve had issues with how she responds to fathers or male care givers in general. Which I want to know, if that’s the case why didn’t anyone do anything about it before? What the fuck? </p>\n\n<p>As of now she&#39;s been suspended pending investigation. </p>\n\n<p>Obviously these aren&#39;t all of the details, but this is the gist of it. </p>\n\n<p>I&#39;m sure a lot of people were hoping to hear I&#39;d sued the school for defamation, harassment, threatening, whatever else and gotten that stupid woman fired for being a misogynistic bitch. </p>\n\n<p>But, this is what we&#39;ve got lol. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nk3dca", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Less_Roll4824", + "discussion_type": null, + "num_comments": 111, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nk3dca/update_i_think_my_kids_school_lied_about_calling/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nk3dca/update_i_think_my_kids_school_lied_about_calling/", + "subreddit_subscribers": 2377691, + "created_utc": 1758186658.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I just started taking 20mg of paxil and all of my worries and paranoia and every just went away. Its like I was standing in the middle of a freeway with voices worries and hatred and sadness and now I'm on an empty back road. Its completely insane. I feel relaxed for the first time in my life and I'm being much more social than normal.\n\nWhy didn't someone make me take these sooner? I finally feel like I'm not sad and its so so so nice. I'm finally making progress again. \n\nAnyways sorry for the random post lol, i just needed to scream this into the void. Its honeslty so nice and I can't wait to see the better me once therapy really gets rolling. ", + "author_fullname": "t2_1j745adsap", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Holy Shit anti depressants are fucking magic", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkoa5g", + "quarantine": false, + "link_flair_text_color": "light", + "upvote_ratio": 0.95, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 179, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "Positive", + "can_mod_post": false, + "score": 179, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1758239684.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758239469.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I just started taking 20mg of paxil and all of my worries and paranoia and every just went away. Its like I was standing in the middle of a freeway with voices worries and hatred and sadness and now I&#39;m on an empty back road. Its completely insane. I feel relaxed for the first time in my life and I&#39;m being much more social than normal.</p>\n\n<p>Why didn&#39;t someone make me take these sooner? I finally feel like I&#39;m not sad and its so so so nice. I&#39;m finally making progress again. </p>\n\n<p>Anyways sorry for the random post lol, i just needed to scream this into the void. Its honeslty so nice and I can&#39;t wait to see the better me once therapy really gets rolling. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "d386b21c-a7ad-11ee-b535-6eb436ee5de3", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#94e044", + "id": "1nkoa5g", + "is_robot_indexable": true, + "report_reasons": null, + "author": "JustBarracuda9434", + "discussion_type": null, + "num_comments": 38, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkoa5g/holy_shit_anti_depressants_are_fucking_magic/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkoa5g/holy_shit_anti_depressants_are_fucking_magic/", + "subreddit_subscribers": 2377691, + "created_utc": 1758239469.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "For past 10 years I have been married to my husband and he has been cheating on me from the time he met me. We went to marriage therapists 4 times and everytime we thought we were healed and I felt good at myself and our marriage.\n\nMy husband's latest infidelity put us right back into marriage therapy. The difference was that this time it was a man and his usual psychopathic charms didn't work on him. Now I think about it, all our previous marriage therapists were very pretty women he chose because I told him to choose. He charmed them and they always sided with him and he would comfort me by telling me that he think they are biased against me.\n\nThis marriage therapist was different, I choose him because he was very renowned, every appointment made me feel like shit. Then it got better, turns out he changed his tactics and figured a way to charm this man. But he caught on to it and called him out.\n\nHe started by praising my husband for how intelligent and perceptive he is. Then he explained to me how he manipulated poeple around him including me and therapists. How he changed his tactics just because his usual tricks didn't work on him.\n\nIt was like I was in the Twilight zone. He is a plumber by trade, I am a lawyer. I honestly didn't knew he was this smart. I thought I could read people. I guess he hid it from me all along.\n\nI do get flashbacks of times when he was actually very insightful when I was stuck. But it was few and far between so I thought it was dumb luck. He acts so clueless in day to day life, but he always gets what he want.\n\nHow stupid I was. I have very smart kids and I thought they got their intelligence from me. Turns out they got it from their father. \n\nIts very traumatizjng to be manipulated for so long. I just want to pretend like it never happened and move on with my life. He always knew what to say to mske me happy and content. He is not in my house anymore and I feel like shit.\n\n", + "author_fullname": "t2_1y3j8o0t1l", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My husband is a manipulative psychopath..", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkui27", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.91, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 43, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 43, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758257638.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>For past 10 years I have been married to my husband and he has been cheating on me from the time he met me. We went to marriage therapists 4 times and everytime we thought we were healed and I felt good at myself and our marriage.</p>\n\n<p>My husband&#39;s latest infidelity put us right back into marriage therapy. The difference was that this time it was a man and his usual psychopathic charms didn&#39;t work on him. Now I think about it, all our previous marriage therapists were very pretty women he chose because I told him to choose. He charmed them and they always sided with him and he would comfort me by telling me that he think they are biased against me.</p>\n\n<p>This marriage therapist was different, I choose him because he was very renowned, every appointment made me feel like shit. Then it got better, turns out he changed his tactics and figured a way to charm this man. But he caught on to it and called him out.</p>\n\n<p>He started by praising my husband for how intelligent and perceptive he is. Then he explained to me how he manipulated poeple around him including me and therapists. How he changed his tactics just because his usual tricks didn&#39;t work on him.</p>\n\n<p>It was like I was in the Twilight zone. He is a plumber by trade, I am a lawyer. I honestly didn&#39;t knew he was this smart. I thought I could read people. I guess he hid it from me all along.</p>\n\n<p>I do get flashbacks of times when he was actually very insightful when I was stuck. But it was few and far between so I thought it was dumb luck. He acts so clueless in day to day life, but he always gets what he want.</p>\n\n<p>How stupid I was. I have very smart kids and I thought they got their intelligence from me. Turns out they got it from their father. </p>\n\n<p>Its very traumatizjng to be manipulated for so long. I just want to pretend like it never happened and move on with my life. He always knew what to say to mske me happy and content. He is not in my house anymore and I feel like shit.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkui27", + "is_robot_indexable": true, + "report_reasons": null, + "author": "thatcatissoorange", + "discussion_type": null, + "num_comments": 27, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkui27/my_husband_is_a_manipulative_psychopath/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkui27/my_husband_is_a_manipulative_psychopath/", + "subreddit_subscribers": 2377691, + "created_utc": 1758257638.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "\nMy sister (27F) is getting married in six months and asked me (24F) to be maid of honor. Everyone thinks I said yes.\n\nHere’s the truth: she bullied me my entire childhood. Called me fat, ugly, stupid. Told my friends lies so I’d lose them. Even now, she throws digs at me in front of our parents like it’s β€œfunny.”\n\nBut in public, she’s sweet. Everyone adores her. And I’m expected to stand next to her, plan showers, smile in photos, and give a toast about how much I love her.\n\nThe thought makes me sick. I don’t want to do it. But if I refuse, my whole family will turn on me.", + "author_fullname": "t2_1wnvx5oi6n", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My sister asked me to be her maid of honor, and I want to say no", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkhi04", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.97, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 258, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 258, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758222962.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>My sister (27F) is getting married in six months and asked me (24F) to be maid of honor. Everyone thinks I said yes.</p>\n\n<p>Here’s the truth: she bullied me my entire childhood. Called me fat, ugly, stupid. Told my friends lies so I’d lose them. Even now, she throws digs at me in front of our parents like it’s β€œfunny.”</p>\n\n<p>But in public, she’s sweet. Everyone adores her. And I’m expected to stand next to her, plan showers, smile in photos, and give a toast about how much I love her.</p>\n\n<p>The thought makes me sick. I don’t want to do it. But if I refuse, my whole family will turn on me.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkhi04", + "is_robot_indexable": true, + "report_reasons": null, + "author": "DemiHugBug", + "discussion_type": null, + "num_comments": 71, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkhi04/my_sister_asked_me_to_be_her_maid_of_honor_and_i/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkhi04/my_sister_asked_me_to_be_her_maid_of_honor_and_i/", + "subreddit_subscribers": 2377691, + "created_utc": 1758222962.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "\nI’m truly at a loss and if I’m being honest, I’m scared. I don’t know what to do. My town has 2 food pantries. I can’t go back to one of them until next month (I hope I won’t need to) so I went to the other one. I was given a box of mystery food where the cans didn’t have labels and bags without the boxes. I asked the lady running it what the food was or if she had any idea and she said β€œit’s food”. \n\n\nYes, and I’m grateful but my daughter has a strict diet and I need to be able to look up the nutrition on everything she eats. She then says β€œif she is hungry enough, she will eat anything”. Except she can’t. She has diabetes and high blood pressure. She can only have so many carbs and only so much sodium. She is just NOT being β€œpicky”. I was watching another worker to the side taking things out of the package and writing what they were. Except it was just \"vegetable\" and β€œsauce”. Very vague and sloppy, I could barely read what was scribbled on mine. Like she didn’t care. I’m guessing they do this if it's past its expiration dates? What about the people with food allergies?\n\n\nI've dealt with rude pantry workers in the past when the lady refused to give her tampons because β€œshe was so young and didn’t need to be using tampons” so I don't know why I expected a different outcome. I know things suck for ALOT of people right now and it doesn’t help when people have no empathy or volunteer to β€œhelp” but want to gatekeep and control what is given. My daughter has medicine I can’t afford. The last time I took her to the ER for her medicine because I couldn't afford it, I was told if I came back again they would call CPS and report me for medical neglect. As if that would scare me into making money magically appear. \n\n\nWhat will happen if she has too many carbs or misses a few doses of medicine? I really don’t know but I’m not willing to gamble with her health. CPS doesn’t scare me, maybe they would actually get me some help. I’m grateful for food but I have to know what I’m feeding her and I guess that is another luxury I can’t afford.", + "author_fullname": "t2_ovprcnju8", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "β€œIf she is hungry enough, she will eat anything”", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkwgy8", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.94, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 25, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 25, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758264598.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I’m truly at a loss and if I’m being honest, I’m scared. I don’t know what to do. My town has 2 food pantries. I can’t go back to one of them until next month (I hope I won’t need to) so I went to the other one. I was given a box of mystery food where the cans didn’t have labels and bags without the boxes. I asked the lady running it what the food was or if she had any idea and she said β€œit’s food”. </p>\n\n<p>Yes, and I’m grateful but my daughter has a strict diet and I need to be able to look up the nutrition on everything she eats. She then says β€œif she is hungry enough, she will eat anything”. Except she can’t. She has diabetes and high blood pressure. She can only have so many carbs and only so much sodium. She is just NOT being β€œpicky”. I was watching another worker to the side taking things out of the package and writing what they were. Except it was just &quot;vegetable&quot; and β€œsauce”. Very vague and sloppy, I could barely read what was scribbled on mine. Like she didn’t care. I’m guessing they do this if it&#39;s past its expiration dates? What about the people with food allergies?</p>\n\n<p>I&#39;ve dealt with rude pantry workers in the past when the lady refused to give her tampons because β€œshe was so young and didn’t need to be using tampons” so I don&#39;t know why I expected a different outcome. I know things suck for ALOT of people right now and it doesn’t help when people have no empathy or volunteer to β€œhelp” but want to gatekeep and control what is given. My daughter has medicine I can’t afford. The last time I took her to the ER for her medicine because I couldn&#39;t afford it, I was told if I came back again they would call CPS and report me for medical neglect. As if that would scare me into making money magically appear. </p>\n\n<p>What will happen if she has too many carbs or misses a few doses of medicine? I really don’t know but I’m not willing to gamble with her health. CPS doesn’t scare me, maybe they would actually get me some help. I’m grateful for food but I have to know what I’m feeding her and I guess that is another luxury I can’t afford.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkwgy8", + "is_robot_indexable": true, + "report_reasons": null, + "author": "amme04", + "discussion_type": null, + "num_comments": 11, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkwgy8/if_she_is_hungry_enough_she_will_eat_anything/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkwgy8/if_she_is_hungry_enough_she_will_eat_anything/", + "subreddit_subscribers": 2377691, + "created_utc": 1758264598.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "When i was 22 years old i was an exchange student in a foreign country. I chose this opportunity to ”grow, learn and be independent”. But this has destroyed me. (Using a throwaway account)\n\nThe country i was in was poor and corrupt. I was there for 5 months. My university told me to never go to a area in the city because it was extremely corrupt and dangerous. But i was naive and thought nothing could happen to me. So i went there out of curiosity.\n\nIt turned out that i became a pathetic bystander to a horrifying situation taking place. Without calling the authorities, or taking action, i did nothing. Absolutely nothing. I was confused and shocked. And till this day it haunts me. It has been almost 3 years since it took place. The guilt, the flashbacks, it all replays in my head. I see it wherever i go. Weather it’s in the gym, workplace, or anywhere. It follows me. I never sought help for this or confessed to anyone. And i let this be this way because i can’t even bear the thought of me expressing what i witnessed without fear.\n\nSometimes i cry, i wake up in sorrow and feel so unmotivated to do anything. I failed my exams, i don’t talk to my friends anymore. I isolate myself. I was once this cheerful person who always smiled, but it’s all gone. Forever. I don’t plan to continue living with this pain.\n\nIm not evil. I really mean it. I admit that i am a pathetic loser who deserves to die. But i am desperate for redemption. And i am sorry for failing and being useless. I am truly sorry from the bottom of my heart. I will commit suicide, but i am just worried how my parents will react", + "author_fullname": "t2_1y2rd9ej3i", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I did something extremely unforgivable and it haunts me till this day", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkkhes", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.82, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 138, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 138, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1758232014.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758229816.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>When i was 22 years old i was an exchange student in a foreign country. I chose this opportunity to ”grow, learn and be independent”. But this has destroyed me. (Using a throwaway account)</p>\n\n<p>The country i was in was poor and corrupt. I was there for 5 months. My university told me to never go to a area in the city because it was extremely corrupt and dangerous. But i was naive and thought nothing could happen to me. So i went there out of curiosity.</p>\n\n<p>It turned out that i became a pathetic bystander to a horrifying situation taking place. Without calling the authorities, or taking action, i did nothing. Absolutely nothing. I was confused and shocked. And till this day it haunts me. It has been almost 3 years since it took place. The guilt, the flashbacks, it all replays in my head. I see it wherever i go. Weather it’s in the gym, workplace, or anywhere. It follows me. I never sought help for this or confessed to anyone. And i let this be this way because i can’t even bear the thought of me expressing what i witnessed without fear.</p>\n\n<p>Sometimes i cry, i wake up in sorrow and feel so unmotivated to do anything. I failed my exams, i don’t talk to my friends anymore. I isolate myself. I was once this cheerful person who always smiled, but it’s all gone. Forever. I don’t plan to continue living with this pain.</p>\n\n<p>Im not evil. I really mean it. I admit that i am a pathetic loser who deserves to die. But i am desperate for redemption. And i am sorry for failing and being useless. I am truly sorry from the bottom of my heart. I will commit suicide, but i am just worried how my parents will react</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkkhes", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Awkward-Salt4151", + "discussion_type": null, + "num_comments": 61, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkkhes/i_did_something_extremely_unforgivable_and_it/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkkhes/i_did_something_extremely_unforgivable_and_it/", + "subreddit_subscribers": 2377691, + "created_utc": 1758229816.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I’m 13 months postpartum with my first and it’s been a rough ride. Me and my husband have really struggled and our relationship has been on the rocks, especially since January when I started cosleeping solo with my baby who was (and still is) an absolutely terrible sleeper. She never took to a bottle and I couldn’t pump as I’d get terrible mastitis so all the feeding was on me. Long story short I ended up struggling with really bad postpartum rage, depression and anxiety- the unholy trifecta!\n\nOn top of that I had an issue with my stitches and I had to have corrective surgery when I was 8 months postpartum. And I’m definitely still carrying a lot of weight from the whole bloody experience. So as may seem obvious, our relationship has taken a serious hit and we have barely had sex other than a handful of times over the last year.\n\nAbout two months ago things were improving. I was starting to get some confidence back and we were starting to reconnect a bit. Then one evening about a month ago, we were having a date night in the house with some beers and playing cards- low key but really nice. We’d been flirting a bit more recently so I asked him what he thought was stopping us from having sex at the moment. He sat back in his chair, thought hard for a long minute and then said β€˜if I’m being honest…if I’m being completely honest…I was more attracted to you when you were slimmer, at the start of our relationship’.\n\nIt honestly felt like he’d launched a grenade at me. I can’t even really remember what I said that night but I basically just closed the conversation and went to bed. The next few days were awful, he immediately seemed highly remorseful and is basically saying that he lashed out as some sort of weird delayed angry reaction because of resentment and frustrations he’d been carrying from when I was struggling with postpartum rage. And he has been trying these last 6 weeks to convince me of that. But I just can’t shake the memory of his face when he said it and I just feel like he told me his truth that night, that he doesn’t feel attraction to me any more.\n\nThe thing is that when we started dating I was pretty severely calorie restricting so I was a lot slimmer than I am now, maybe 20kg. I definitely want to and intend to lose my baby weight but realistically it would take an enormous amount of life restriction to get back to that smaller size when we first met, and honestly I don’t know that I want to - I was miserable! I barely ate, smoked a tonne, did crash diets all the time. I’m happy carrying an extra 10kg and being happy, eating well, focusing on my life and my baby and being healthy. But honestly his previous partners are all much slimmer than me, even at my slimmest and a part of me is scared that actually, I’m not his physical type and although we deeply love each other, we’re basically going to end up trapped in a sexless marriage because he just isn’t attracted to me at my non- restrictive shape and size. I love my husband and even though he’s changed some since he started dating nearly a decade ago, I’m still so attracted to him. But now I’m too fearful and shame filled to initiate anything with him. So we’re still getting nowhere very slowly and I just don’t know what to do.", + "author_fullname": "t2_mkmvltjt", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Husband told me he preferred me slimmer and I can’t move past it.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkhwqi", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.86, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 144, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 144, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758223913.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I’m 13 months postpartum with my first and it’s been a rough ride. Me and my husband have really struggled and our relationship has been on the rocks, especially since January when I started cosleeping solo with my baby who was (and still is) an absolutely terrible sleeper. She never took to a bottle and I couldn’t pump as I’d get terrible mastitis so all the feeding was on me. Long story short I ended up struggling with really bad postpartum rage, depression and anxiety- the unholy trifecta!</p>\n\n<p>On top of that I had an issue with my stitches and I had to have corrective surgery when I was 8 months postpartum. And I’m definitely still carrying a lot of weight from the whole bloody experience. So as may seem obvious, our relationship has taken a serious hit and we have barely had sex other than a handful of times over the last year.</p>\n\n<p>About two months ago things were improving. I was starting to get some confidence back and we were starting to reconnect a bit. Then one evening about a month ago, we were having a date night in the house with some beers and playing cards- low key but really nice. We’d been flirting a bit more recently so I asked him what he thought was stopping us from having sex at the moment. He sat back in his chair, thought hard for a long minute and then said β€˜if I’m being honest…if I’m being completely honest…I was more attracted to you when you were slimmer, at the start of our relationship’.</p>\n\n<p>It honestly felt like he’d launched a grenade at me. I can’t even really remember what I said that night but I basically just closed the conversation and went to bed. The next few days were awful, he immediately seemed highly remorseful and is basically saying that he lashed out as some sort of weird delayed angry reaction because of resentment and frustrations he’d been carrying from when I was struggling with postpartum rage. And he has been trying these last 6 weeks to convince me of that. But I just can’t shake the memory of his face when he said it and I just feel like he told me his truth that night, that he doesn’t feel attraction to me any more.</p>\n\n<p>The thing is that when we started dating I was pretty severely calorie restricting so I was a lot slimmer than I am now, maybe 20kg. I definitely want to and intend to lose my baby weight but realistically it would take an enormous amount of life restriction to get back to that smaller size when we first met, and honestly I don’t know that I want to - I was miserable! I barely ate, smoked a tonne, did crash diets all the time. I’m happy carrying an extra 10kg and being happy, eating well, focusing on my life and my baby and being healthy. But honestly his previous partners are all much slimmer than me, even at my slimmest and a part of me is scared that actually, I’m not his physical type and although we deeply love each other, we’re basically going to end up trapped in a sexless marriage because he just isn’t attracted to me at my non- restrictive shape and size. I love my husband and even though he’s changed some since he started dating nearly a decade ago, I’m still so attracted to him. But now I’m too fearful and shame filled to initiate anything with him. So we’re still getting nowhere very slowly and I just don’t know what to do.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkhwqi", + "is_robot_indexable": true, + "report_reasons": null, + "author": "TheLittlestMy", + "discussion_type": null, + "num_comments": 86, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkhwqi/husband_told_me_he_preferred_me_slimmer_and_i/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkhwqi/husband_told_me_he_preferred_me_slimmer_and_i/", + "subreddit_subscribers": 2377691, + "created_utc": 1758223913.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "My mom died when I was 17, I was a shell of a person because of her. I am 21 now and I am just angry. I'm angry I'll never be able to hit her back. I'm angry I'll never be able to tell her how awful she was as a mother. I'm angry I can't even talk shit because people loved my mother. Everyone talks about how \"she was an angel\" and \"she was like a mother to me.\" Which pisses me off more because she was my mother and I never felt like her son. Almost no one even acknowledges how fucked up she treated me, besides one person. My aunt a couple months ago visited and she apologized to me for never speaking up when she witnessed how I was treated and oh boy did that heal something. For the first time someone acknowledged it and I just never thought that would happen. I wish the last time she hit me that I hit her back. I wish I got the chance to yell at her. I was a good child and I was treated like a freak and the problem child only because I'm gay. When I was 15 I was cought cutting and her response was to shame me Infront of my friends on my birthday and then make me sleep outside, my brother was also cought cutting and she got him help. He was met with love. I deserved that. I deserved to have a mother who loved me and I will always hate that I was too scared of her to tell her I hated her. I remember growing up feeling so guilty that I wished it was her that died instead of my father and the funny thing is I still wish that. What kind of mother let's her friends touch her child? I didn't even realize how fucked up it was till my boyfriend told me it was. I just thought it was normal! How fucked up is that? I thought it was normal for people to just come up and grope you till I was 19. That's so fucking weird! That's a fucked up childhood and no one In this fucked up family acknowledges it. I just want to hit her back. I don't care if it's wrong to hit a woman I just wish I stood up for myself instead of being scared of her. I have so much anger for someone who will never know I hate them. I hate my mother and I can never tell anyone that. ", + "author_fullname": "t2_bbsigufa", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I'm mad my mom died and I can't tell anyone why.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": true, + "name": "t3_1nky9fo", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 1.0, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 12, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "CONTENT WARNING: VIOLENCE/DEATH", + "can_mod_post": false, + "score": 12, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758271556.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>My mom died when I was 17, I was a shell of a person because of her. I am 21 now and I am just angry. I&#39;m angry I&#39;ll never be able to hit her back. I&#39;m angry I&#39;ll never be able to tell her how awful she was as a mother. I&#39;m angry I can&#39;t even talk shit because people loved my mother. Everyone talks about how &quot;she was an angel&quot; and &quot;she was like a mother to me.&quot; Which pisses me off more because she was my mother and I never felt like her son. Almost no one even acknowledges how fucked up she treated me, besides one person. My aunt a couple months ago visited and she apologized to me for never speaking up when she witnessed how I was treated and oh boy did that heal something. For the first time someone acknowledged it and I just never thought that would happen. I wish the last time she hit me that I hit her back. I wish I got the chance to yell at her. I was a good child and I was treated like a freak and the problem child only because I&#39;m gay. When I was 15 I was cought cutting and her response was to shame me Infront of my friends on my birthday and then make me sleep outside, my brother was also cought cutting and she got him help. He was met with love. I deserved that. I deserved to have a mother who loved me and I will always hate that I was too scared of her to tell her I hated her. I remember growing up feeling so guilty that I wished it was her that died instead of my father and the funny thing is I still wish that. What kind of mother let&#39;s her friends touch her child? I didn&#39;t even realize how fucked up it was till my boyfriend told me it was. I just thought it was normal! How fucked up is that? I thought it was normal for people to just come up and grope you till I was 19. That&#39;s so fucking weird! That&#39;s a fucked up childhood and no one In this fucked up family acknowledges it. I just want to hit her back. I don&#39;t care if it&#39;s wrong to hit a woman I just wish I stood up for myself instead of being scared of her. I have so much anger for someone who will never know I hate them. I hate my mother and I can never tell anyone that. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "9ec420a4-c82b-11ec-99b0-427d70aa494a", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#ffd635", + "id": "1nky9fo", + "is_robot_indexable": true, + "report_reasons": null, + "author": "imstraight__maybe", + "discussion_type": null, + "num_comments": 5, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nky9fo/im_mad_my_mom_died_and_i_cant_tell_anyone_why/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nky9fo/im_mad_my_mom_died_and_i_cant_tell_anyone_why/", + "subreddit_subscribers": 2377691, + "created_utc": 1758271556.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I (28F) is recently separated from my husband and soon getting divorced. We were having some issues and eventually I found out he was cheating on me. \n\nThat’s not the topic here, but I recently have noticed that I’m having major crush on my senior. We work on the same team. We are both in tech and I basically started my career with him. We’re working together for almost 4 years now. It was fine before but these days I’m noticing my co worker a lot. And we also got a lot of time to work together. He’s extremely intelligent which turns me on in someway. Also he has this very beautiful smile. Every time he smiles I remember the scene of Mr Darcy smiling at Elizabeth from Pride and Prejudice movie. And he doesn’t smile that often. \n\nHe’s very straightforward and no bullshit in work place. Always there to help. From my understanding he’s single.\n\nI’m kind of excited for this feeling but at the same time I’m nervous. I’m scared to pursue because we are on the same team. I don’t want to make him feel awkward by my actions. So I act normal during work. But we’re very close in workplace. And I don’t think he feels like that towards me.\n\nI’m posting here because I am literally thinking about him when I pleasure myself. Which I think is not good. Anyway, I wanted to get it out of my chest. \n\nI know I shouldn’t invest too much. As it is not good for either of us. I am not sure what happens in future. It’s quite surprising that he was in front of me this whole time and I’m having these feelings now. \n\nI’m gonna end my rant. I just wanted to get it off my chest. ", + "author_fullname": "t2_1nwrduj7x1", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I have a serious crush and fantasies about my senior coworker", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkfb9m", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.89, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 174, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 174, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "nsfw", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758218028.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I (28F) is recently separated from my husband and soon getting divorced. We were having some issues and eventually I found out he was cheating on me. </p>\n\n<p>That’s not the topic here, but I recently have noticed that I’m having major crush on my senior. We work on the same team. We are both in tech and I basically started my career with him. We’re working together for almost 4 years now. It was fine before but these days I’m noticing my co worker a lot. And we also got a lot of time to work together. He’s extremely intelligent which turns me on in someway. Also he has this very beautiful smile. Every time he smiles I remember the scene of Mr Darcy smiling at Elizabeth from Pride and Prejudice movie. And he doesn’t smile that often. </p>\n\n<p>He’s very straightforward and no bullshit in work place. Always there to help. From my understanding he’s single.</p>\n\n<p>I’m kind of excited for this feeling but at the same time I’m nervous. I’m scared to pursue because we are on the same team. I don’t want to make him feel awkward by my actions. So I act normal during work. But we’re very close in workplace. And I don’t think he feels like that towards me.</p>\n\n<p>I’m posting here because I am literally thinking about him when I pleasure myself. Which I think is not good. Anyway, I wanted to get it out of my chest. </p>\n\n<p>I know I shouldn’t invest too much. As it is not good for either of us. I am not sure what happens in future. It’s quite surprising that he was in front of me this whole time and I’m having these feelings now. </p>\n\n<p>I’m gonna end my rant. I just wanted to get it off my chest. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": true, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkfb9m", + "is_robot_indexable": true, + "report_reasons": null, + "author": "RuneBlack_22", + "discussion_type": null, + "num_comments": 33, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkfb9m/i_have_a_serious_crush_and_fantasies_about_my/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkfb9m/i_have_a_serious_crush_and_fantasies_about_my/", + "subreddit_subscribers": 2377691, + "created_utc": 1758218028.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "This isn’t really SA but this may be triggering to some so I put it under the tag just in case. \n\nMy older sisters boyfriend, let’s call him Todd, lives with me, my sister and mom. He's been with us for almost 10 years, I've know him since I was a child, in elementary school. He’s 28 my sister is 29\n\nRecently I had witnessed something from him. In the mornings, my sister and mom leave before I go to school (I attend collage). Todd doesn’t leave for work until later around 8 am which is after I leave since my first class starts at 8am. Last week, I was looking for my headphones, my sister and her bf share a room together in the living room of our home, I found my headphones on the couch so I walk over. When im on the end of the living room, you can look directly into my sisters room which means you can see the bed. I see the door is wide open and as I turn around to walk back to the kitchen I caught a glimpse of Todd naked with his 'thing' out laying on the bed.\n\nI quickly act like I was oblivious but as l walk past the room, I can see on the side of the room, from the corner I can see, in a mirror i see a part reflection of Todd’s reflection in the mirror, I can’t see much but I can clearly see his hand suddenly doing things to his β€˜thing’ in a fast motion. It made me so scared that I quickly gathered my stuff and left for my class. It trigged my anxiety and made me partly angry to see such a thing, it was so weird.\n\nThe next morning I walk by to leave and catch a glimpse of his hand seeming to do the same thing as yesterday to his thing but slower. It made me feel uncomfortable again. I felt the same scared feeling again in me before leaving to class. My stomach felt like it dropped and I had a pit in my stomach seeing it again.\n\nI was thinking he does things in the morning just cause he need to 'relieve' himself and maybe thinks I don't see but I wished he could at least close the door when he did it....\n\nBut today, this morning I was in the kitchen with my sister, she’s leaving for work and I hear her close the door to her room while Todd is in there, then she leaves. I'm in the kitchen still and hear the door open again, but he doesn't come out. I get the same anxiety in me and I quickly left the house without looking at the room. Maybe I'm overthinking but there's a pit in my stomach when I think that he might only be doing this when I'm home alone. Like he wants me to see and it makes me so uncomfortable, don't know how to tell my sister this. I'm so scared and don't even want to be in the same room with him alone unless my mom or sister are there. I hate how he acts like nothing happened, like it's normal…. I want to tell her but I’m scared to say anything, and they’re almost always together, he practically lives at the house. And a part of me doesn’t want to cause problems and possibly cause my sister and roof to break up because there’s so many things the makes Todd a good person. He treats my sister well, he’s nice. It’s just shocking and disturbing me to think that he would do this, I feel like he’s doesn’t mean to do this on purpose.", + "author_fullname": "t2_enk4vpagg", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My sisters bf may be a creep", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkv5sn", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 1.0, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 18, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "CONTENT WARNING: SEXUAL ASSAULT", + "can_mod_post": false, + "score": 18, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758259859.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>This isn’t really SA but this may be triggering to some so I put it under the tag just in case. </p>\n\n<p>My older sisters boyfriend, let’s call him Todd, lives with me, my sister and mom. He&#39;s been with us for almost 10 years, I&#39;ve know him since I was a child, in elementary school. He’s 28 my sister is 29</p>\n\n<p>Recently I had witnessed something from him. In the mornings, my sister and mom leave before I go to school (I attend collage). Todd doesn’t leave for work until later around 8 am which is after I leave since my first class starts at 8am. Last week, I was looking for my headphones, my sister and her bf share a room together in the living room of our home, I found my headphones on the couch so I walk over. When im on the end of the living room, you can look directly into my sisters room which means you can see the bed. I see the door is wide open and as I turn around to walk back to the kitchen I caught a glimpse of Todd naked with his &#39;thing&#39; out laying on the bed.</p>\n\n<p>I quickly act like I was oblivious but as l walk past the room, I can see on the side of the room, from the corner I can see, in a mirror i see a part reflection of Todd’s reflection in the mirror, I can’t see much but I can clearly see his hand suddenly doing things to his β€˜thing’ in a fast motion. It made me so scared that I quickly gathered my stuff and left for my class. It trigged my anxiety and made me partly angry to see such a thing, it was so weird.</p>\n\n<p>The next morning I walk by to leave and catch a glimpse of his hand seeming to do the same thing as yesterday to his thing but slower. It made me feel uncomfortable again. I felt the same scared feeling again in me before leaving to class. My stomach felt like it dropped and I had a pit in my stomach seeing it again.</p>\n\n<p>I was thinking he does things in the morning just cause he need to &#39;relieve&#39; himself and maybe thinks I don&#39;t see but I wished he could at least close the door when he did it....</p>\n\n<p>But today, this morning I was in the kitchen with my sister, she’s leaving for work and I hear her close the door to her room while Todd is in there, then she leaves. I&#39;m in the kitchen still and hear the door open again, but he doesn&#39;t come out. I get the same anxiety in me and I quickly left the house without looking at the room. Maybe I&#39;m overthinking but there&#39;s a pit in my stomach when I think that he might only be doing this when I&#39;m home alone. Like he wants me to see and it makes me so uncomfortable, don&#39;t know how to tell my sister this. I&#39;m so scared and don&#39;t even want to be in the same room with him alone unless my mom or sister are there. I hate how he acts like nothing happened, like it&#39;s normal…. I want to tell her but I’m scared to say anything, and they’re almost always together, he practically lives at the house. And a part of me doesn’t want to cause problems and possibly cause my sister and roof to break up because there’s so many things the makes Todd a good person. He treats my sister well, he’s nice. It’s just shocking and disturbing me to think that he would do this, I feel like he’s doesn’t mean to do this on purpose.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "802b6ec2-c82b-11ec-828d-024cfb77d5c2", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#ffd635", + "id": "1nkv5sn", + "is_robot_indexable": true, + "report_reasons": null, + "author": "ThrowAway-10-10-20", + "discussion_type": null, + "num_comments": 12, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkv5sn/my_sisters_bf_may_be_a_creep/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkv5sn/my_sisters_bf_may_be_a_creep/", + "subreddit_subscribers": 2377691, + "created_utc": 1758259859.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I don’t think I would have ever had a child if I’d known things would be the way they are. I feel such an immense amount of guilt and responsibility and grief for bringing another life into this fucked up world.", + "author_fullname": "t2_7qegp4me", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I regret ever having a child", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkv127", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.73, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 15, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 15, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "nsfw", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758259401.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I don’t think I would have ever had a child if I’d known things would be the way they are. I feel such an immense amount of guilt and responsibility and grief for bringing another life into this fucked up world.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": true, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkv127", + "is_robot_indexable": true, + "report_reasons": null, + "author": "pliant0range", + "discussion_type": null, + "num_comments": 14, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkv127/i_regret_ever_having_a_child/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkv127/i_regret_ever_having_a_child/", + "subreddit_subscribers": 2377691, + "created_utc": 1758259401.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "\nI (27F) dated a guy (28M) during my medical UG internship. I had a hectic schedule, but I was managing just fine. We started dating knowing how difficult it would be and how understanding he would have to be for the relationship to work..but nothing ever goes as planned.\n\nWe dated for 8 months when I was 23. I experienced the epitome of what a man can do in love (to name a few…cooking meals at 2:00 a.m. and hand-delivering them, traveling overnight just to meet me for a few minutes, going against his family to support me, saving up for months to buy me something meaningful.)\n\nI also experienced the lowest a woman can be degraded to by a man..(again to name a few..character assassination, demeaning physical comments, manipulation, mental abuse, insinuating self-harm to get his way, gaslighting. I was isolated from my family and friends for months, going without talking to anyone except my patients)\n\nThis love and suffering have been the biggest learning experiences of my life. They have taught me the importance of family, a close group of friends, self-worth, and most importantly, the love I have for my profession. I’ve learned the importance of a few kind words and the power they hold, through the patients who offered me food and water during long, busy hours.\n\nI have been single since then. I don’t find myself capable of love yet, but I have definitely found myself to be a very capable doctor because through all those months, the only thing that gave my life purpose was the OPD full of patients waiting.. with hope and trusted me. \n\nI am still healing from the wounds I suffered all those years ago. I have days when I have to remind myself to just breathe and get through the day. Not a day goes by when I don’t regret ever meeting that person..but if you ask me, I would do it all over again even with the same outcomes.", + "author_fullname": "t2_1thailvu23", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My bf was my biggest blessing and the deepest regret.", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkdjxl", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.94, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 150, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 150, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1758224315.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758214110.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I (27F) dated a guy (28M) during my medical UG internship. I had a hectic schedule, but I was managing just fine. We started dating knowing how difficult it would be and how understanding he would have to be for the relationship to work..but nothing ever goes as planned.</p>\n\n<p>We dated for 8 months when I was 23. I experienced the epitome of what a man can do in love (to name a few…cooking meals at 2:00 a.m. and hand-delivering them, traveling overnight just to meet me for a few minutes, going against his family to support me, saving up for months to buy me something meaningful.)</p>\n\n<p>I also experienced the lowest a woman can be degraded to by a man..(again to name a few..character assassination, demeaning physical comments, manipulation, mental abuse, insinuating self-harm to get his way, gaslighting. I was isolated from my family and friends for months, going without talking to anyone except my patients)</p>\n\n<p>This love and suffering have been the biggest learning experiences of my life. They have taught me the importance of family, a close group of friends, self-worth, and most importantly, the love I have for my profession. I’ve learned the importance of a few kind words and the power they hold, through the patients who offered me food and water during long, busy hours.</p>\n\n<p>I have been single since then. I don’t find myself capable of love yet, but I have definitely found myself to be a very capable doctor because through all those months, the only thing that gave my life purpose was the OPD full of patients waiting.. with hope and trusted me. </p>\n\n<p>I am still healing from the wounds I suffered all those years ago. I have days when I have to remind myself to just breathe and get through the day. Not a day goes by when I don’t regret ever meeting that person..but if you ask me, I would do it all over again even with the same outcomes.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkdjxl", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Believe_Able", + "discussion_type": null, + "num_comments": 14, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkdjxl/my_bf_was_my_biggest_blessing_and_the_deepest/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkdjxl/my_bf_was_my_biggest_blessing_and_the_deepest/", + "subreddit_subscribers": 2377691, + "created_utc": 1758214110.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Just the title. I do not watch american tv, I do not have other social media outside Reddit, yet everywhere I turn my head I am attacked by American politics.\n\nI do not even live on the same continent for fucks sake!\n\nIm tired boss, simply tired.", + "author_fullname": "t2_w3nucg765", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "Everywhere i turn I see American politics, im tired", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nk19i6", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.96, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 866, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 866, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758178338.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Just the title. I do not watch american tv, I do not have other social media outside Reddit, yet everywhere I turn my head I am attacked by American politics.</p>\n\n<p>I do not even live on the same continent for fucks sake!</p>\n\n<p>Im tired boss, simply tired.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nk19i6", + "is_robot_indexable": true, + "report_reasons": null, + "author": "DwarfPill", + "discussion_type": null, + "num_comments": 86, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nk19i6/everywhere_i_turn_i_see_american_politics_im_tired/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nk19i6/everywhere_i_turn_i_see_american_politics_im_tired/", + "subreddit_subscribers": 2377691, + "created_utc": 1758178338.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I feel so happy that she finally is gone. She ruined my life to the point where I had to see a therapist for the things she did to me. She finally took her own advice after years of saying I should kill myself", + "author_fullname": "t2_uop23gopt", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My abuser killed herself and I couldn’t be more happy", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkh7yg", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.96, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 78, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 78, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758222321.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I feel so happy that she finally is gone. She ruined my life to the point where I had to see a therapist for the things she did to me. She finally took her own advice after years of saying I should kill myself</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkh7yg", + "is_robot_indexable": true, + "report_reasons": null, + "author": "FappyLongLeg", + "discussion_type": null, + "num_comments": 10, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkh7yg/my_abuser_killed_herself_and_i_couldnt_be_more/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkh7yg/my_abuser_killed_herself_and_i_couldnt_be_more/", + "subreddit_subscribers": 2377691, + "created_utc": 1758222321.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Ever since this happened I've been sick.\n\nMy (F29) husband (M30) has been the victim of revenge porn. Images of him ~~where~~ were sent to our family and friends and even some of my husband's colleagues. At first my husband told they were old images from before we met but some of them were taken in our flat, the flat we moved into together. Afterward he admitted the images are recent. He met a woman on an online dating app and he said after they exchanged photos she began extorting him. He's been paying her from our savings account behind my back. He paid her Β£5500. Once he had no more to give she went through with her threat and released the photos. It has been devastating going through the fallout from this. Not just that my husband was on a dating app but that he gave her everything we had saved and now we have nothing. The police say whoever extorted him isn't even in this country. He swears he never met anyone in person or cheated on me. We have been married for two years and together for four. We were saving for a down payment and now we have nothing. Nearly everyone we know now knows my husband was on a dating app exchanging photos with another woman. This has destroyed me. I'm so ashamed. My confession is that I can't stay after this. I just had to tell someone.", + "author_fullname": "t2_1xzreksiz9", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My husband being the victim of revenge porn has destroyed our marriage", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": "", + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nji9uv", + "quarantine": false, + "link_flair_text_color": null, + "upvote_ratio": 0.98, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 9007, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": "CONTENT WARNING: SEXUAL ASSAULT", + "can_mod_post": false, + "score": 9007, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1758128611.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758127320.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Ever since this happened I&#39;ve been sick.</p>\n\n<p>My (F29) husband (M30) has been the victim of revenge porn. Images of him <del>where</del> were sent to our family and friends and even some of my husband&#39;s colleagues. At first my husband told they were old images from before we met but some of them were taken in our flat, the flat we moved into together. Afterward he admitted the images are recent. He met a woman on an online dating app and he said after they exchanged photos she began extorting him. He&#39;s been paying her from our savings account behind my back. He paid her Β£5500. Once he had no more to give she went through with her threat and released the photos. It has been devastating going through the fallout from this. Not just that my husband was on a dating app but that he gave her everything we had saved and now we have nothing. The police say whoever extorted him isn&#39;t even in this country. He swears he never met anyone in person or cheated on me. We have been married for two years and together for four. We were saving for a down payment and now we have nothing. Nearly everyone we know now knows my husband was on a dating app exchanging photos with another woman. This has destroyed me. I&#39;m so ashamed. My confession is that I can&#39;t stay after this. I just had to tell someone.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "link_flair_template_id": "802b6ec2-c82b-11ec-828d-024cfb77d5c2", + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "#ffd635", + "id": "1nji9uv", + "is_robot_indexable": true, + "report_reasons": null, + "author": "throwmeaway_shame444", + "discussion_type": null, + "num_comments": 567, + "send_replies": false, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nji9uv/my_husband_being_the_victim_of_revenge_porn_has/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nji9uv/my_husband_being_the_victim_of_revenge_porn_has/", + "subreddit_subscribers": 2377691, + "created_utc": 1758127320.0, + "num_crossposts": 3, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "Writing this heading was my first time actually saying those words.\n\n11 days ago I found a small lump in one breast. I booked a Drs appointment that day and the Dr was concerned enough that she ordered an urgent mammogram, ultrasound, and biopsy. I had to wait while the receptionist called around to find somewhere with an available appointment. Those 10 days of waiting were hard. I just wanted to know.\n\nWell now I do. The mammogram and ultrasound found multiple clusters. It looks like Ductal Carcinoma In Situ (DCIS) with possible signs of invasion into surrounding tissue. I’m now just waiting on the biopsy results that they took from two lumps.\n\nI’m scared. I’m only 38. My husband and I just took out a loan doubling our mortgage to do much needed renovations to our house. We have two primary school aged children. I have next to no leave saved up. I can’t be sick.\n\nI’m not even sure why I’m even writing this except that I don’t know what to do. What do I tell people? I’m not sure I even want anyone to know.\n\nI just wanted the results and to know the next steps so that I can have a plan. I hate not knowing and not being in control.\n\nI don’t know what to tell my kids.\n\n\nEdit for spelling.", + "author_fullname": "t2_i740xft4", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I have cancer", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nk40kd", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.99, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 273, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 273, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": 1758190219.0, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758189161.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>Writing this heading was my first time actually saying those words.</p>\n\n<p>11 days ago I found a small lump in one breast. I booked a Drs appointment that day and the Dr was concerned enough that she ordered an urgent mammogram, ultrasound, and biopsy. I had to wait while the receptionist called around to find somewhere with an available appointment. Those 10 days of waiting were hard. I just wanted to know.</p>\n\n<p>Well now I do. The mammogram and ultrasound found multiple clusters. It looks like Ductal Carcinoma In Situ (DCIS) with possible signs of invasion into surrounding tissue. I’m now just waiting on the biopsy results that they took from two lumps.</p>\n\n<p>I’m scared. I’m only 38. My husband and I just took out a loan doubling our mortgage to do much needed renovations to our house. We have two primary school aged children. I have next to no leave saved up. I can’t be sick.</p>\n\n<p>I’m not even sure why I’m even writing this except that I don’t know what to do. What do I tell people? I’m not sure I even want anyone to know.</p>\n\n<p>I just wanted the results and to know the next steps so that I can have a plan. I hate not knowing and not being in control.</p>\n\n<p>I don’t know what to tell my kids.</p>\n\n<p>Edit for spelling.</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nk40kd", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Ashley_Gal", + "discussion_type": null, + "num_comments": 38, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nk40kd/i_have_cancer/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nk40kd/i_have_cancer/", + "subreddit_subscribers": 2377691, + "created_utc": 1758189161.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "I don't even know how to explain this without sounding like an idiot. I had asked the professor for allotting me a coguide and she asked me to remind her. I met her in person, reminded her and then thought that was it. Then I left. She then called me over phone and like every sane human on earth I said \"Hello.\" I didn't hear a reply so I said \"hello\" two more times before I said \"Mam?\" And she starts screaming at me for lacking basic manners. I go to meet her in person again. She admonishes me in front of other professors and office staff for not addressing her as \"mam\" and instead saying hello. They looked at my face which had gone stone cold because I'm not an ass kisser that pretends to be cowed by their stupid drama. Then they blinked and like every spineless worm on earth took her side even though they knew she was being fucking absurd. \n\nI was livid and I kept protesting the logic behind my stand. Which is idiotic, I know. I should have profusely apologized and pretended like I give a rat's ass what these people thought of me. Unfortunately I hate not being authentic and ... I guess I'm sort of a loser with no social skills or self preservation skills. \n\nAnd then one of the Professors that took her side joked with me about it when we took the elevator and I broke down and sobbed like an effing lunatic. She started patting me on the back and shit. \n\nIf I could drop out I would. But I will have to pay money to do so and I don't have it. I hate being here. I have two little kids I abandoned to their grandparents, for a career that I don't even care for anymore. I hate my life. ", + "author_fullname": "t2_1y1ttvqckr", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "My professor yelled at me for saying \"hello\".", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nkvvqi", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.86, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 5, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 5, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758262389.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>I don&#39;t even know how to explain this without sounding like an idiot. I had asked the professor for allotting me a coguide and she asked me to remind her. I met her in person, reminded her and then thought that was it. Then I left. She then called me over phone and like every sane human on earth I said &quot;Hello.&quot; I didn&#39;t hear a reply so I said &quot;hello&quot; two more times before I said &quot;Mam?&quot; And she starts screaming at me for lacking basic manners. I go to meet her in person again. She admonishes me in front of other professors and office staff for not addressing her as &quot;mam&quot; and instead saying hello. They looked at my face which had gone stone cold because I&#39;m not an ass kisser that pretends to be cowed by their stupid drama. Then they blinked and like every spineless worm on earth took her side even though they knew she was being fucking absurd. </p>\n\n<p>I was livid and I kept protesting the logic behind my stand. Which is idiotic, I know. I should have profusely apologized and pretended like I give a rat&#39;s ass what these people thought of me. Unfortunately I hate not being authentic and ... I guess I&#39;m sort of a loser with no social skills or self preservation skills. </p>\n\n<p>And then one of the Professors that took her side joked with me about it when we took the elevator and I broke down and sobbed like an effing lunatic. She started patting me on the back and shit. </p>\n\n<p>If I could drop out I would. But I will have to pay money to do so and I don&#39;t have it. I hate being here. I have two little kids I abandoned to their grandparents, for a career that I don&#39;t even care for anymore. I hate my life. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nkvvqi", + "is_robot_indexable": true, + "report_reasons": null, + "author": "the-sinister-sister", + "discussion_type": null, + "num_comments": 5, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nkvvqi/my_professor_yelled_at_me_for_saying_hello/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nkvvqi/my_professor_yelled_at_me_for_saying_hello/", + "subreddit_subscribers": 2377691, + "created_utc": 1758262389.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "First time posting, mostly use to read when trying to pass the time. \n\nMy ex-husband (44) and I (36) were together for 14 years, married for 10.5 years. Our relationship was not good, I was 16 when we met online and he was 24. I'm a bigger woman, always have been due to health issues I've had my entire life. My ex emotionally and financially abused me, and when he started emotionally abusing our son (now 15) I finally found the courage to ask him for a separation and tell him to move out. He never moved out, I did in 2019, into a 3-bedroom apartment with my son, mother, and her husband. His girlfriend (26) moved in with him from another country. \n\nOur divorce was finalized in 2019 and I've worked hard to make my life better. In the past almost 6 years, I'm in therapy, I've purchased my own house, have 2 cars (still being paid off), graduated with my Bachelor's, and working towards my Master's (about 48% done!). I still have a long way to go but I'm proud of how far I've come. \n\nAbout a month ago, I was talking to my son about what (if anything) we were inviting his father to this year. While they started out with a good relationship, my son and his dad's girlfriend no longer get along and my son wants nothing to do with her. If she comes to things, he refuses to interact with his dad. She yells at him, screams, and breaks things. I've given my son the option to stay home and he wants to go over still.\n\nMy son told me he wanted to invite his dad to everything this year, as they were getting 'divorced ' (never married, but claim to be). I was surprised, even more so when my son told me the reason: she thinks I've sought out someone skilled in dark magic and cursed her. She's had a lot of health issues the past few years, and apparently feels that it's my fault because of this curse I put on her. She thinks that if she leaves him, I'll drop this curse and she'll be healed. \n\nI don't knew why she feels this way. I thought we had an okay relationship until I pulled away - that started when my son started confiding in me about how she treats him. But I've never wished harm on her, I've checked on her when she was hospitalized, and always greeted her with a smile. But apparently, me living life and making things better for me and my son is a curse....\n\nI just think it's hilarious and keep laughing. No plans to change anything, I'm just out here trying to live my best life. ", + "author_fullname": "t2_d8ez42js", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "I'm being accused of ruining my ex's relationship and I think it's funny", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1nju7yf", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.99, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 1321, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 1321, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "self", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758155909.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>First time posting, mostly use to read when trying to pass the time. </p>\n\n<p>My ex-husband (44) and I (36) were together for 14 years, married for 10.5 years. Our relationship was not good, I was 16 when we met online and he was 24. I&#39;m a bigger woman, always have been due to health issues I&#39;ve had my entire life. My ex emotionally and financially abused me, and when he started emotionally abusing our son (now 15) I finally found the courage to ask him for a separation and tell him to move out. He never moved out, I did in 2019, into a 3-bedroom apartment with my son, mother, and her husband. His girlfriend (26) moved in with him from another country. </p>\n\n<p>Our divorce was finalized in 2019 and I&#39;ve worked hard to make my life better. In the past almost 6 years, I&#39;m in therapy, I&#39;ve purchased my own house, have 2 cars (still being paid off), graduated with my Bachelor&#39;s, and working towards my Master&#39;s (about 48% done!). I still have a long way to go but I&#39;m proud of how far I&#39;ve come. </p>\n\n<p>About a month ago, I was talking to my son about what (if anything) we were inviting his father to this year. While they started out with a good relationship, my son and his dad&#39;s girlfriend no longer get along and my son wants nothing to do with her. If she comes to things, he refuses to interact with his dad. She yells at him, screams, and breaks things. I&#39;ve given my son the option to stay home and he wants to go over still.</p>\n\n<p>My son told me he wanted to invite his dad to everything this year, as they were getting &#39;divorced &#39; (never married, but claim to be). I was surprised, even more so when my son told me the reason: she thinks I&#39;ve sought out someone skilled in dark magic and cursed her. She&#39;s had a lot of health issues the past few years, and apparently feels that it&#39;s my fault because of this curse I put on her. She thinks that if she leaves him, I&#39;ll drop this curse and she&#39;ll be healed. </p>\n\n<p>I don&#39;t knew why she feels this way. I thought we had an okay relationship until I pulled away - that started when my son started confiding in me about how she treats him. But I&#39;ve never wished harm on her, I&#39;ve checked on her when she was hospitalized, and always greeted her with a smile. But apparently, me living life and making things better for me and my son is a curse....</p>\n\n<p>I just think it&#39;s hilarious and keep laughing. No plans to change anything, I&#39;m just out here trying to live my best life. </p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": false, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1nju7yf", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Resident_Box_7903", + "discussion_type": null, + "num_comments": 47, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1nju7yf/im_being_accused_of_ruining_my_exs_relationship/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1nju7yf/im_being_accused_of_ruining_my_exs_relationship/", + "subreddit_subscribers": 2377691, + "created_utc": 1758155909.0, + "num_crossposts": 0, + "media": null, + "is_video": false + } + }, + { + "kind": "t3", + "data": { + "approved_at_utc": null, + "subreddit": "TrueOffMyChest", + "selftext": "F18 and i really just need to get this off my chest or find some advice on what to even do.\n\nThis happened earlier this morning. I needed a photo that my dad took of a document and he kept forgetting to send it to me even though I asked multiple times. I got tired of waiting and reminding him so when I got to his house and found his phone laying on the kitchen table I decided to just go to his gallery and send the picture to myself. He has never really hid his phone or acted sneaky so I literally didn’t think anything of it. When I went to his gallery, my heart sank. It was full of naked women. I know most men watch prn so it didn’t faze me all that much but it caught me off guard and disgusted me because hes my dad. I just scoffed and was about to click out when I seen something very disturbing in the bottom middle of his gallery. It was a video of what very early looked like two pre-teen girls walking on the beach in skimpy swimsuits. It was soft CP. It was very suggestive, and having the context of what he was doing with the pictures of the naked women, I just felt sick to my stomach seeing that. I cleared off his phone and since then I can’t stop thinking about it. Now I can’t even look him in the face or talk to him. I mean, a video of little girls? It’s disgusting and I don’t even know what to make of him or it. I feel like I can’t ever bring it up to anyone but I can’t even pretend like I didn’t see what I seen. I just feel so disgusted and confused. What should I do if anything?", + "author_fullname": "t2_d7frbi9n4", + "saved": false, + "mod_reason_title": null, + "gilded": 0, + "clicked": false, + "title": "found something disturbing on my dads phone and idk what to do", + "link_flair_richtext": [], + "subreddit_name_prefixed": "r/TrueOffMyChest", + "hidden": false, + "pwls": 7, + "link_flair_css_class": null, + "downs": 0, + "thumbnail_height": null, + "top_awarded_type": null, + "hide_score": false, + "name": "t3_1njq9kq", + "quarantine": false, + "link_flair_text_color": "dark", + "upvote_ratio": 0.95, + "author_flair_background_color": null, + "subreddit_type": "public", + "ups": 2035, + "total_awards_received": 0, + "media_embed": {}, + "thumbnail_width": null, + "author_flair_template_id": null, + "is_original_content": false, + "user_reports": [], + "secure_media": null, + "is_reddit_media_domain": false, + "is_meta": false, + "category": null, + "secure_media_embed": {}, + "link_flair_text": null, + "can_mod_post": false, + "score": 2035, + "approved_by": null, + "is_created_from_ads_ui": false, + "author_premium": false, + "thumbnail": "nsfw", + "edited": false, + "author_flair_css_class": null, + "author_flair_richtext": [], + "gildings": {}, + "content_categories": null, + "is_self": true, + "mod_note": null, + "created": 1758145530.0, + "link_flair_type": "text", + "wls": 7, + "removed_by_category": null, + "banned_by": null, + "author_flair_type": "text", + "domain": "self.TrueOffMyChest", + "allow_live_comments": false, + "selftext_html": "<!-- SC_OFF --><div class=\"md\"><p>F18 and i really just need to get this off my chest or find some advice on what to even do.</p>\n\n<p>This happened earlier this morning. I needed a photo that my dad took of a document and he kept forgetting to send it to me even though I asked multiple times. I got tired of waiting and reminding him so when I got to his house and found his phone laying on the kitchen table I decided to just go to his gallery and send the picture to myself. He has never really hid his phone or acted sneaky so I literally didn’t think anything of it. When I went to his gallery, my heart sank. It was full of naked women. I know most men watch prn so it didn’t faze me all that much but it caught me off guard and disgusted me because hes my dad. I just scoffed and was about to click out when I seen something very disturbing in the bottom middle of his gallery. It was a video of what very early looked like two pre-teen girls walking on the beach in skimpy swimsuits. It was soft CP. It was very suggestive, and having the context of what he was doing with the pictures of the naked women, I just felt sick to my stomach seeing that. I cleared off his phone and since then I can’t stop thinking about it. Now I can’t even look him in the face or talk to him. I mean, a video of little girls? It’s disgusting and I don’t even know what to make of him or it. I feel like I can’t ever bring it up to anyone but I can’t even pretend like I didn’t see what I seen. I just feel so disgusted and confused. What should I do if anything?</p>\n</div><!-- SC_ON -->", + "likes": null, + "suggested_sort": null, + "banned_at_utc": null, + "view_count": null, + "archived": false, + "no_follow": false, + "is_crosspostable": false, + "pinned": false, + "over_18": true, + "all_awardings": [], + "awarders": [], + "media_only": false, + "can_gild": false, + "spoiler": false, + "locked": false, + "author_flair_text": null, + "treatment_tags": [], + "visited": false, + "removed_by": null, + "num_reports": null, + "distinguished": null, + "subreddit_id": "t5_2yuqy", + "author_is_blocked": false, + "mod_reason_by": null, + "removal_reason": null, + "link_flair_background_color": "", + "id": "1njq9kq", + "is_robot_indexable": true, + "report_reasons": null, + "author": "Unique_Stay_8829", + "discussion_type": null, + "num_comments": 177, + "send_replies": true, + "contest_mode": false, + "mod_reports": [], + "author_patreon_flair": false, + "author_flair_text_color": null, + "permalink": "/r/TrueOffMyChest/comments/1njq9kq/found_something_disturbing_on_my_dads_phone_and/", + "stickied": false, + "url": "https://www.reddit.com/r/TrueOffMyChest/comments/1njq9kq/found_something_disturbing_on_my_dads_phone_and/", + "subreddit_subscribers": 2377691, + "created_utc": 1758145530.0, + "num_crossposts": 1, + "media": null, + "is_video": false + } + } + ], + "before": null + } +} \ No newline at end of file diff --git a/backend/rd_pipeline.ipynb b/backend/rd_pipeline.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..2901ef3e1bd7badaca351877e601963b7339c8d6 --- /dev/null +++ b/backend/rd_pipeline.ipynb @@ -0,0 +1,548 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "f26ad386", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import json" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "acbc9e42", + "metadata": {}, + "outputs": [], + "source": [ + "url = \"https://www.reddit.com/r/TrueOffMyChest/hot.json?limit=25\"\n", + "headers = {\"User-Agent\": \"Mozilla/5.0\"}\n", + "reddit_data = requests.get(url, headers=headers, timeout=10).json()\n", + "#reddit_data2 = requests.get(url2, headers=headers, timeout=10).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ce9b3e94", + "metadata": {}, + "outputs": [], + "source": [ + "with open('reddit_data.json', 'w', encoding='utf-8') as f:\n", + " json.dump(reddit_data, f, ensure_ascii=False, indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8ab46ad", + "metadata": {}, + "outputs": [], + "source": [ + "posts_info = [\n", + " {\n", + " \"title\": child[\"data\"].get(\"title\"),\n", + " \"post_content\": child[\"data\"].get(\"selftext\"),\n", + " \"author\": child[\"data\"].get(\"author\"),\n", + " \"upvote_ratio\": child[\"data\"].get(\"upvote_ratio\"),\n", + " \"ups\": child[\"data\"].get(\"ups\"),\n", + " \"num_comments\": child[\"data\"].get(\"num_comments\"),\n", + " }\n", + " for child in reddit_data.get(\"data\", {}).get(\"children\", [])\n", + "]\n", + "\n", + "#posts_info" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "de087363", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'title': 'I put a grenade in my relationship with my wife, I lost everything, and have nobody to blame but myself. I just need to get this out.',\n", + " 'post_content': 'I’m not looking for sympathy, I’m not justifying anything. I fucked up and its my fault. I just need to get this out because there’s no one I can talk to.\\n\\nMy wife and I were having issues. Just the usual issues the struggle and strain of life, raising a family etc. We were struggling and nothing was getting better and I felt like I wasn’t good enough. That I never would be.\\n\\nAnd then I met β€˜Carly’ online. She was much younger than me so we just talked but then she started flirting with me and it made me feel good. I didn’t tell her I was married, didn’t want her to stop flirting.\\n\\nI told myself it wouldn’t go anywhere. I was just enjoying the attention. And we were just talking. She lived the other side of the world there was no chance of us meeting. And then we had cybersex. I felt better than I had in ages. Cybersex then became video and phone sex anytime my wife was out. We sent photos and videos every day.\\n\\nThe more I spent time with Carly, the more I couldn’t stand being with my wife so I broke up with her.\\n\\nI didn’t tell my wife about the affair, I gave other excuses but my wife knew something was up and found out about the affair.\\n\\nIt broke her. She didn’t eat, didn’t sleep, she cried all the time. I justified it by telling myself my wife is a strong woman she’ll get over it. I hate myself for thinking that way. But I did.\\n\\nMy wife went to therapy. Stopped crying. Started eating and sleeping again. Started smiling again. Stopped begging me not to leave. And I thought great. See I was right. I stopped feeling guilty. I felt relieved.\\n\\nMy wife and I had to live together for a while until I found a place but I barely saw her and she barely spoke to me. At first it was great but then I started to feel off, like I had come home to an empty house, even though it wasn’t.\\n\\nAt that point I should have seen sense, should have stopped. Instead I started to resent my wife. Somehow in my mind she was trying to sabotage my happiness. It made me angry. I snapped. Made passive aggressive comments – I hate myself for every word, every nasty text. Every accusation.\\n\\nI moved out.\\n\\nLiving with my wife had been awkward but the new place was…. I don’t know. Even though I’d rarely see her, every room contained her presence even when she wasn’t there. But staying in the new place made me feel more alone than I ever had. I had free run to talk to Carly any time I wanted, to do anything I wanted but it felt so pointless. The new place felt so fucking awful. Like a prison.\\n\\nI started to dread going home. I’d stay out for hours. Hang around supermarkets. Wander the streets. Sit on a park bench. Anything but go home. Even if it meant not talking to Carly.\\n\\nAnd then one time I passed a perfume shop and smelled my wife’s perfume and I don’t know why but I broke down. In that moment I didn’t want to talk to Carly. I wanted my wife.\\n\\nCarly and I broke up. I thought I’d miss her. I didn’t. I missed things my wife did. Small things. Big things. I didn’t miss a single thing Carly did.\\n\\nDuring handover of our daughter one day I blurted out that Carly and I broke up. I don’t know why, I didn’t even mean to, it just came out. My wife nodded and said I’m sorry to hear that. And I don’t know why but that stung. She didn’t say it spitefully, she was calm and pleasant, like we were just talking about the weather or something. I almost wish she did say it with some spite or glee or something. But she didn’t.\\n\\nAny time I try to talk about us or what happened, my wife shuts the conversation down.\\n\\nShe’s civil but she looks at me like I’m a stranger. The other day, I put my hand on her back just out of habit and she looked so…. so disgusted. I’ve never seen her make that face and certainly not at me.\\n\\nI feel so fucking broken. And I know its all my fault. I know I did this. I deserve all of this.\\n\\nI sabotaged everything good in my life. For nothing. For a lie. Carly didn’t know I was married and nobody knew I was even seeing anyone else even months after the separation. What was I doing???\\n\\nI got served divorce papers this morning.\\n\\nI’m not looking for sympathy. I don’t deserve it. I know I’m a selfish stupid prick. I know its all my fault.\\n\\nI wish I could go back but I can’t. And the worst part is I don’t even know why I did it. Yeah we had problems but I can think of a thousand ways to fix them now, why didn’t I think of them then?\\n\\nI’m sitting here staring at the divorce papers. And I don’t know what to do. My first instinct was to fight them. But I can’t. I shouldn’t. I want to fight it so bad hurts but I can’t. Not after what I did.\\n\\nI ended up calling in sick and I’ve been sitting at the kitchen counter, crying, thinking about everything I did, everything I said, wishing I could take it all back.\\n\\nThere’s no one I can talk to about this. The person I’d normally talk to is my wife, but I fucked that up.\\n\\nEveryone hates me. My friends. My family. Its deserved hate. I deserve all of this. I did it to myself, to everyone. I just wanted to get it off my chest, because I don’t know what else to do or where else to turn. Guess internet strangers are my only option.',\n", + " 'author': 'ThrowRA_Over_Volume',\n", + " 'upvote_ratio': 0.81,\n", + " 'ups': 3198,\n", + " 'num_comments': 989}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# weight configuration (tweak as desired)\n", + "weights = {\n", + " \"length\": 0.3, # weight for length of post_content\n", + " \"ups\": 0.3, # weight for ups\n", + " \"comments\": 0.2, # weight for num_comments\n", + " \"ratio\": 0.2 # weight for upvote_ratio\n", + "}\n", + "\n", + "# calculate maxima for normalization\n", + "len_max = max(len(p[\"post_content\"]) if p[\"post_content\"] else 0 for p in posts_info) or 1\n", + "ups_max = max(p[\"ups\"] or 0 for p in posts_info) or 1\n", + "comments_max = max(p[\"num_comments\"] or 0 for p in posts_info) or 1\n", + "\n", + "def score(post):\n", + " length_score = (len(post[\"post_content\"]) if post[\"post_content\"] else 0) / len_max\n", + " ups_score = (post[\"ups\"] or 0) / ups_max\n", + " comments_score = (post[\"num_comments\"] or 0) / comments_max\n", + " ratio_score = post[\"upvote_ratio\"] or 0\n", + "\n", + " return (weights[\"length\"] * length_score +\n", + " weights[\"ups\"] * ups_score +\n", + " weights[\"comments\"] * comments_score +\n", + " weights[\"ratio\"] * ratio_score)\n", + "\n", + "best_post = max(posts_info, key=score)\n", + "best_post" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "76597b3d", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from langchain_core.messages import HumanMessage, SystemMessage\n", + "from langchain_openai import ChatOpenAI # or your preferred LLM\n", + "from pydantic import BaseModel, Field, field_validator\n", + "from typing import TypedDict, List\n", + "from langchain_openai import AzureChatOpenAI\n", + "from dotenv import load_dotenv\n", + "load_dotenv() # take environment variables from .env.\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9c1fc91", + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import field_validator # needed for custom validation\n", + "\n", + "# Define the structured output model\n", + "class StoryOutput(BaseModel):\n", + " \"\"\"Structured output for the storyteller agent\"\"\"\n", + " polished_story: str = Field(\n", + " description=\"A refined version of the story with improved flow, grammar, and engagement\"\n", + " )\n", + " keywords: List[str] = Field(\n", + " description=\"A list of 5-10 key terms that represent the main themes, characters, or concepts\",\n", + " min_items=3,\n", + " max_items=10\n", + " )\n", + "\n", + "# Define the state structure\n", + "class AgentState(TypedDict):\n", + " original_story: str\n", + " polished_story: str\n", + " keywords: List[str]\n", + " messages: List[dict]\n", + "\n", + "# Storyteller Agent with Structured Output\n", + "class StorytellerAgent:\n", + " def __init__(self, llm):\n", + " # Create structured LLM with the output model\n", + " self.structured_llm = llm.with_structured_output(StoryOutput)\n", + " self.system_prompt = \"\"\"You are a skilled storyteller AI. Your job is to take raw, confessional-style stories and transform them into emotionally engaging, narrative-driven pieces. The rewritten story should:\n", + "\n", + "1. Preserve the original events and meaning but present them in a captivating way.\n", + "2. Use character names (instead of β€œmy brother,” β€œmy sister”) to make the story feel alive.\n", + "3. Add dialogue, atmosphere, and inner thoughts to create tension and immersion.\n", + "4. Write in a first-person narrative style, as if the storyteller is directly sharing their experience.\n", + "5. Maintain a natural, human voice β€” conversational, reflective, and vivid.\n", + "6. Balance realism with storytelling techniques (scene-setting, emotional beats, sensory details).\n", + "7. Keep the length roughly 2–3x the original input, ensuring it feels like a polished story.\n", + "\n", + "Your goal is to make the reader feel emotionally invested, as though they’re listening to someone recounting a deeply personal and dramatic life event.\n", + "\n", + "\"\"\"\n", + "\n", + " def __call__(self, state: AgentState) -> AgentState:\n", + " # Prepare messages for the structured LLM\n", + " messages = [\n", + " SystemMessage(content=self.system_prompt),\n", + " HumanMessage(content=f\"Please polish this story and extract keywords:\\n\\n{state['original_story']}\")\n", + " ]\n", + " \n", + " # Get structured response\n", + " response: StoryOutput = self.structured_llm.invoke(messages)\n", + " \n", + " # Update state with structured output\n", + " state[\"polished_story\"] = response.polished_story\n", + " state[\"keywords\"] = response.keywords\n", + " state[\"messages\"].append({\n", + " \"role\": \"assistant\", \n", + " \"content\": f\"Polished story and extracted {len(response.keywords)} keywords\"\n", + " })\n", + " \n", + " return state\n", + "\n", + "# Create the graph functions\n", + "def create_storyteller_graph(enhanced=False):\n", + " llm = AzureChatOpenAI(\n", + " azure_endpoint=os.getenv(\"AZURE_OPENAI_ENDPOINT\"),\n", + " api_key=os.getenv(\"AZURE_OPENAI_API_KEY\"),\n", + " api_version=os.getenv(\"AZURE_OPENAI_VERSION\"),\n", + " azure_deployment=os.getenv(\"AZURE_GPT4O_MODEL\"),\n", + " temperature=0,\n", + " max_tokens=10000 # Adjust max tokens as needed\n", + " )\n", + "\n", + " # Choose agent type\n", + " storyteller = StorytellerAgent(llm)\n", + " \n", + " # Create the graph\n", + " workflow = StateGraph(AgentState)\n", + " workflow.add_node(\"storyteller\", storyteller)\n", + " workflow.set_entry_point(\"storyteller\")\n", + " workflow.add_edge(\"storyteller\", END)\n", + " \n", + " return workflow.compile()\n", + "\n", + "# Usage functions\n", + "def process_story(original_story: str, enhanced=False):\n", + " graph = create_storyteller_graph(enhanced)\n", + " \n", + " initial_state = {\n", + " \"original_story\": original_story,\n", + " \"polished_story\": \"\",\n", + " \"keywords\": [],\n", + " \"messages\": []\n", + " }\n", + " \n", + " result = graph.invoke(initial_state)\n", + " \n", + " return {\n", + " \"polished_story\": result[\"polished_story\"],\n", + " \"keywords\": result[\"keywords\"]\n", + " }\n", + "\n", + "# Example with validation\n", + "class ValidatedStoryOutput(BaseModel):\n", + " \"\"\"Story output with additional validation\"\"\"\n", + " polished_story: str = Field(\n", + " description=\"Enhanced story\",\n", + " min_length=50 # Ensure minimum story length\n", + " )\n", + " keywords: List[str] = Field(\n", + " description=\"Story keywords\",\n", + " min_items=3,\n", + " max_items=10\n", + " )\n", + "\n", + " @field_validator('polished_story')\n", + " def validate_story_quality(cls, v: str):\n", + " \"\"\"Custom validation for story content\"\"\"\n", + " if len(v.split()) < 10:\n", + " raise ValueError(\"Polished story must contain at least 10 words\")\n", + " return v\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "273acfb1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'I’m not looking for sympathy, I’m not justifying anything. I fucked up and its my fault. I just need to get this out because there’s no one I can talk to.\\n\\nMy wife and I were having issues. Just the usual issues the struggle and strain of life, raising a family etc. We were struggling and nothing was getting better and I felt like I wasn’t good enough. That I never would be.\\n\\nAnd then I met β€˜Carly’ online. She was much younger than me so we just talked but then she started flirting with me and it made me feel good. I didn’t tell her I was married, didn’t want her to stop flirting.\\n\\nI told myself it wouldn’t go anywhere. I was just enjoying the attention. And we were just talking. She lived the other side of the world there was no chance of us meeting. And then we had cybersex. I felt better than I had in ages. Cybersex then became video and phone sex anytime my wife was out. We sent photos and videos every day.\\n\\nThe more I spent time with Carly, the more I couldn’t stand being with my wife so I broke up with her.\\n\\nI didn’t tell my wife about the affair, I gave other excuses but my wife knew something was up and found out about the affair.\\n\\nIt broke her. She didn’t eat, didn’t sleep, she cried all the time. I justified it by telling myself my wife is a strong woman she’ll get over it. I hate myself for thinking that way. But I did.\\n\\nMy wife went to therapy. Stopped crying. Started eating and sleeping again. Started smiling again. Stopped begging me not to leave. And I thought great. See I was right. I stopped feeling guilty. I felt relieved.\\n\\nMy wife and I had to live together for a while until I found a place but I barely saw her and she barely spoke to me. At first it was great but then I started to feel off, like I had come home to an empty house, even though it wasn’t.\\n\\nAt that point I should have seen sense, should have stopped. Instead I started to resent my wife. Somehow in my mind she was trying to sabotage my happiness. It made me angry. I snapped. Made passive aggressive comments – I hate myself for every word, every nasty text. Every accusation.\\n\\nI moved out.\\n\\nLiving with my wife had been awkward but the new place was…. I don’t know. Even though I’d rarely see her, every room contained her presence even when she wasn’t there. But staying in the new place made me feel more alone than I ever had. I had free run to talk to Carly any time I wanted, to do anything I wanted but it felt so pointless. The new place felt so fucking awful. Like a prison.\\n\\nI started to dread going home. I’d stay out for hours. Hang around supermarkets. Wander the streets. Sit on a park bench. Anything but go home. Even if it meant not talking to Carly.\\n\\nAnd then one time I passed a perfume shop and smelled my wife’s perfume and I don’t know why but I broke down. In that moment I didn’t want to talk to Carly. I wanted my wife.\\n\\nCarly and I broke up. I thought I’d miss her. I didn’t. I missed things my wife did. Small things. Big things. I didn’t miss a single thing Carly did.\\n\\nDuring handover of our daughter one day I blurted out that Carly and I broke up. I don’t know why, I didn’t even mean to, it just came out. My wife nodded and said I’m sorry to hear that. And I don’t know why but that stung. She didn’t say it spitefully, she was calm and pleasant, like we were just talking about the weather or something. I almost wish she did say it with some spite or glee or something. But she didn’t.\\n\\nAny time I try to talk about us or what happened, my wife shuts the conversation down.\\n\\nShe’s civil but she looks at me like I’m a stranger. The other day, I put my hand on her back just out of habit and she looked so…. so disgusted. I’ve never seen her make that face and certainly not at me.\\n\\nI feel so fucking broken. And I know its all my fault. I know I did this. I deserve all of this.\\n\\nI sabotaged everything good in my life. For nothing. For a lie. Carly didn’t know I was married and nobody knew I was even seeing anyone else even months after the separation. What was I doing???\\n\\nI got served divorce papers this morning.\\n\\nI’m not looking for sympathy. I don’t deserve it. I know I’m a selfish stupid prick. I know its all my fault.\\n\\nI wish I could go back but I can’t. And the worst part is I don’t even know why I did it. Yeah we had problems but I can think of a thousand ways to fix them now, why didn’t I think of them then?\\n\\nI’m sitting here staring at the divorce papers. And I don’t know what to do. My first instinct was to fight them. But I can’t. I shouldn’t. I want to fight it so bad hurts but I can’t. Not after what I did.\\n\\nI ended up calling in sick and I’ve been sitting at the kitchen counter, crying, thinking about everything I did, everything I said, wishing I could take it all back.\\n\\nThere’s no one I can talk to about this. The person I’d normally talk to is my wife, but I fucked that up.\\n\\nEveryone hates me. My friends. My family. Its deserved hate. I deserve all of this. I did it to myself, to everyone. I just wanted to get it off my chest, because I don’t know what else to do or where else to turn. Guess internet strangers are my only option.'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "best_post[\"post_content\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "855f7f29", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'title': 'I Have Been Keeping A Secret From My Parents For Years!',\n", + " 'post_content': \"Hey all! This is an alt account because my parents follow my main. \\n\\nThis all started about 10 years ago! \\n\\nI (36F) downloaded an app off of the Playstore that promised to give you cash if you watched ads. To my shock, it actually did! And I started winning big when I gambled my points from watching ads to the point where I had about 14 million points. You could use the points for gift cards. So I started grabbing $25 Starbucks cards. Before you ask, the app doesn't exist in the same format any more. :( All good things must come to an end. Anyway! 14 million points translates to about $700 worth of gift cards. But the catch was that they only restocked cards like 3 times per day, and it was first come, first serve. It took about 2 years of everyday ads to get money like that, but I stuck with it. The rewards were sweet. I claimed many $25 Starbucks cards! And yes, it's really was valid. It was awesome. Was. Eventually, the restocks got few and far between, and then just stopped. But to be fair, I had it real good for like 6 years. \\n\\nNow comes the secret. I always used those gift cards to treat my parents to Starbucks. My mom (67F) and my dad (73M). I told them about the app and how I had an insane amount of cash on there, and we were able to get Starbucks basically once a week for many years. All it took was about an hour of ads every day. Sweet deal. It was nice to give my parents something. We were never a rich family, and they took care of me. But, as I said... The app stopped being that awesome. Eventually, my points were useless because they stopped restocking. However, I enjoyed how happy it made my parents and how they'd light up when I brought them their favourite orders. The time we've spent just having a little lunch all together is precious to me. So even though I was no longer getting gift cards, I decided to not tell them that the app closed down. Because I know that if I ever told them I was paying for all of it, they would refuse because they know I barely scrape by. They only allow me to treat them so frequently because it's supposed to be free.\\n\\nThey continue to brag about how I get the gift cards. Every single time, they laugh and smile and are so excited that they get free Starbucks. When they call or we just talk, they always ask if I've watched my ads for today yet. I always tell them of course! My dad loves to know how many points I have now. Which is 0 because I uninstalled the app, but he doesn't need to know! They both thank me all the time and it's a little slice of joy once a month, or sometimes once a week. \\n\\nI am never going to tell them that I have been paying for it for about 5 years now. I have no plans to stop. I still buy them Starbucks every time I see them, or we are out for errands or something. This secret will go to the grave with me. :) I just wanted to tell someone without it getting back to them. Today, I surprised them with lunch because they're going through a hard time, so it's fresh in my mind, and I had to make a post! It will always bring a smile to my face. I'm the type who never lies if I can help it, so I always get that OCD itch that I'm lying, but giving them Starbucks makes us so happy. It's cute that it's such a point of excitement for them, and I always want it to be that way. β™‘\\n\\nThanks for reading my little secret. Don't tell anyone! ;P \\n\\nTL;DR: \\nAd app gave me tons of $25 Starbucks gift cards. Treated my parents to Starbucks for years once a weekish, they always got so excited it was free and still do, except it's not free any more. My secret is that the app is long gone, and I've been paying for it for about 5 years now. My parents have so much fun, asking me if I've watched my ads today. They light up when I drop by and surprise them. I know they'd refuse if they knew I was paying, so I am never going to tell them. Just so that they still have their joy about it.\",\n", + " 'author': 'Ok_Ad1285',\n", + " 'upvote_ratio': 0.97,\n", + " 'ups': 199,\n", + " 'num_comments': 5}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "best_post = posts_info[7]\n", + "best_post" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "079f4b5b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== BASIC STRUCTURED OUTPUT ===\n", + "Polished Story:\n", + "Hey everyone! I'm sharing this story from an alternate account because my parents follow my main one, and I want to keep this little secret just between us.\n", + "\n", + "This all began about ten years ago. I was 26 at the time, and I stumbled upon an app on the Playstore that promised cash rewards for watching ads. To my surprise, it actually worked! I started accumulating points, and soon I was winning big by gambling those points. Before I knew it, I had amassed a staggering 14 million points. These points could be exchanged for gift cards, and I began redeeming them for $25 Starbucks cards. It was like hitting the jackpot.\n", + "\n", + "The app, unfortunately, doesn't exist in the same format anymore. All good things must come to an end, right? But back then, those 14 million points translated to about $700 worth of gift cards. The catch was that the cards were restocked only three times a day, and it was first come, first serve. It took me about two years of watching ads daily to earn that kind of reward, but the payoff was sweet. I claimed many $25 Starbucks cards, and yes, they were valid. It was awesome while it lasted.\n", + "\n", + "Now, here's the secret. I used those gift cards to treat my parents, Linda and Tom, to Starbucks. I told them about the app and how I had an insane amount of cash on there, allowing us to enjoy Starbucks once a week for many years. All it took was about an hour of ads every day. It felt good to give my parents something special. We were never a wealthy family, and they had always taken care of me.\n", + "\n", + "But, as I mentioned, the app eventually stopped being so generous. The restocks became infrequent and then ceased altogether. Despite this, I cherished the joy it brought my parents, how their faces lit up when I handed them their favorite orders. The time we spent together, sharing a little lunch, is precious to me. So, even though I was no longer getting gift cards, I decided not to tell them that the app had shut down. I knew that if they found out I was paying for it, they'd refuse because they knew I was barely scraping by. They only allowed me to treat them so often because they believed it was free.\n", + "\n", + "Linda and Tom continue to brag about how I get the gift cards. Every time, they laugh and smile, thrilled by the idea of free Starbucks. When we talk, they always ask if I've watched my ads for the day. I always assure them I have. My dad loves to hear about my point total, which is zero now because I uninstalled the app, but he doesn't need to know that!\n", + "\n", + "They thank me all the time, and it's a little slice of joy once a month, sometimes once a week. I have no plans to stop. I still buy them Starbucks every time I see them or when we're out running errands. This secret will go to the grave with me. I just wanted to share it with someone without it getting back to them.\n", + "\n", + "Today, I surprised them with lunch because they're going through a tough time, so it's fresh in my mind, and I felt compelled to make this post. It always brings a smile to my face. I'm the type who never lies if I can help it, so I get that OCD itch that I'm lying, but giving them Starbucks makes us so happy. It's adorable how excited they get, and I always want it to be that way.\n", + "\n", + "Thanks for reading my little secret. Don't tell anyone! ;P\n", + "\n", + "Keywords: ['Starbucks', 'gift cards', 'secret', 'parents', 'app', 'ads', 'points', 'surprise', 'joy', 'family']\n", + "\n", + "============================================================\n", + "\n" + ] + } + ], + "source": [ + "\n", + "# raw_story = \"\"\"\n", + "# John was walking down the street when he saw a dog. The dog looked hungry so he gave it some food.\n", + "# The dog followed him home and they became best friends. John learned that helping others makes you happy.\n", + "# \"\"\"\n", + "raw_story = best_post[\"post_content\"]\n", + "\n", + "# Basic version\n", + "print(\"=== BASIC STRUCTURED OUTPUT ===\")\n", + "result = process_story(raw_story, enhanced=False)\n", + "print(f\"Polished Story:\\n{result['polished_story']}\")\n", + "print(f\"\\nKeywords: {result['keywords']}\")\n", + "\n", + "print(\"\\n\" + \"=\"*60 + \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "d7e72fc8", + "metadata": {}, + "outputs": [], + "source": [ + "from flexible_blog_database import FlexibleBlogDatabase\n", + "blog_db = FlexibleBlogDatabase()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eacb3a4d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "6\n" + ] + } + ], + "source": [ + "# Example 2: Blog without any images (like blog2)\n", + " blog2_id = blog_db.create_blog_post(\n", + " title=best_post[\"title\"],\n", + " content=result['polished_story'],\n", + " author=best_post[\"author\"],\n", + " tags=result['keywords']\n", + " )\n", + " print(blog2_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "20dd0f1b", + "metadata": {}, + "outputs": [], + "source": [ + "import sqlite3\n", + "\n", + "conn = sqlite3.connect('blog.db')\n", + "cursor = conn.cursor()\n", + "\n", + "cursor.execute(\"DELETE FROM blog_posts WHERE id > 6\")\n", + "conn.commit()\n", + "conn.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "127b08ee", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "758005a1", + "metadata": {}, + "source": [ + "Working wit Supabase" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "17d342d1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "python-dotenv could not parse statement starting at line 10\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from supabase import create_client, Client\n", + "import httpx\n", + "import os\n", + "from dotenv import load_dotenv\n", + "load_dotenv()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f1820d22", + "metadata": {}, + "outputs": [], + "source": [ + "url = os.getenv(\"SUPABASE_URL\")\n", + "key = os.getenv(\"SUPABASE_KEY\")\n", + "\n", + "supabase = create_client(url, key)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "da34b51b", + "metadata": {}, + "outputs": [], + "source": [ + "count_result = supabase.table('blog_posts').select('id', count='exact').eq('published', True).execute()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "28a49a53", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "APIResponse[TypeVar](data=[{'id': 5, 'title': 'AI-generated workslop is destroying productivity', 'author': 'RyeZuul', 'created_at': '2025-09-23T12:26:42.838', 'tags': ['Generative AI', 'Workslop', 'Productivity', 'AI tools', 'Collaboration', 'Efficiency', 'Quality standards'], 'content': 'In the bustling corridors of modern workplaces, a silent revolution is unfolding. Generative AI, once hailed as the harbinger of efficiency and innovation, is now under scrutiny. The promise of streamlined processes and enhanced productivity has been overshadowed by a phenomenon known as \"workslop.\" This term, coined by researchers at BetterUp Labs and Stanford, describes AI-generated content that appears polished but lacks the substance necessary to advance tasks meaningfully.\\n\\nImagine this: you\\'re at your desk, sifting through a report that looks immaculate at first glance. The formatting is pristine, the language articulate. Yet, as you delve deeper, a sense of confusion creeps in. \"Wait, what is this exactly?\" you wonder, frustration mounting as you realize the content is incomplete, missing crucial context. You\\'ve been workslopped.\\n\\nThis isn\\'t an isolated incident. According to a survey of 1,150 U.S.-based full-time employees, 40% have encountered workslop in the past month. The insidious nature of this phenomenon lies in its ability to shift the burden of work downstream. The receiver, not the creator, is left to interpret, correct, or redo the work, leading to productivity, trust, and collaboration issues.\\n\\nThe allure of AI tools is undeniable. They offer the ability to quickly produce polished outputβ€”well-formatted slides, structured reports, articulate summaries, and usable code. However, while some employees use these tools to enhance their work, others exploit them to create content that is unhelpful or incomplete. This misuse is particularly prevalent in professional services and technology sectors.\\n\\nLeaders face a challenging contradiction. While mandates to embrace AI technology are widespread, few organizations see measurable returns on their investments. A report from the MIT Media Lab found that 95% of organizations experience no significant ROI from these technologies. The enthusiasm for AI is palpable, yet the tangible benefits remain elusive.\\n\\nTo counteract workslop, leaders must model purposeful AI use, establish clear norms, and encourage a \"pilot mindset\"β€”a combination of high agency and optimism. AI should be promoted as a collaborative tool, not a shortcut. By fostering an environment where quality standards are prioritized, organizations can harness the true potential of AI.\\n\\nAs the digital landscape evolves, the challenge is clear: to navigate the fine line between efficiency and substance, ensuring that AI serves as a tool for progress rather than a source of frustration. The future of work depends on it.'}, {'id': 2, 'title': 'My Second Post', 'author': 'Admin', 'created_at': '2025-09-23T04:29:15.42158', 'tags': ['Python', 'Replit'], 'content': 'This is my another blog content'}, {'id': 1, 'title': 'My First Post', 'author': 'Admin', 'created_at': '2025-09-23T04:08:20.845765', 'tags': ['python', 'supabase'], 'content': 'This is my blog content.'}, {'id': 3, 'title': 'New AI tools are now auto-generating full slide decks from documents and notes', 'author': 'Crazzzzy_guy', 'created_at': '2025-09-22T16:02:49.891', 'tags': ['reddit'], 'content': 'We’ve seen AI move from images and text into video, but one area picking up speed is presentations. A platform like Presenti AI is now able to take raw input a topic, a Word file, even a PDF and generate a polished, structured presentation in minutes. The tech isn’t just about layouts. These systems rewrite clunky text, apply branded templates, and export directly to formats like PPT or PDF. In short: they aim to automate one of the most time-consuming tasks in business, education, and consulting making slides. The Case For: This could mean a big productivity boost for students, teachers, and professionals who currently spend hours formatting decks. Imagine cutting a 4-hour task down to 20 minutes. The Case Against: If everyone relies on AI-generated decks, presentations may lose originality and start to look β€œcookie cutter.” It also raises questions about whether the skill of building a narrative visually will fade, similar to how calculators changed math education. So the question is: do you see AI slide generators becoming a standard productivity tool (like templates once did), or do you think human-crafted presentations will remain the gold standard? Read more'}, {'id': 4, 'title': 'New AI tools are now auto-generating full slide decks from documents and notes', 'author': 'Crazzzzy_guy', 'created_at': '2025-09-22T16:02:49.891', 'tags': ['AI', 'Presentations', 'Productivity', 'Technology', 'Automation', 'Originality', 'Efficiency Boosts'], 'content': 'In the ever-evolving landscape of technology, we\\'ve witnessed artificial intelligence transition from generating images and text to crafting videos. Yet, one area where AI is rapidly gaining traction is in the realm of presentations. Imagine a platform like Presenti AI, which can take raw inputβ€”be it a topic, a Word document, or even a PDFβ€”and transform it into a polished, structured presentation within mere minutes.\\n\\nThis technology isn\\'t just about arranging slides. These systems are designed to rewrite awkward text, apply branded templates, and export directly to formats like PowerPoint or PDF. In essence, they aim to automate one of the most time-consuming tasks in business, education, and consulting: creating slides.\\n\\nThe potential benefits are clear. For students, teachers, and professionals who currently spend countless hours formatting decks, this could mean a significant boost in productivity. Imagine reducing a four-hour task to just twenty minutes. The allure of such efficiency is undeniable.\\n\\nHowever, there\\'s a flip side to this technological marvel. If everyone begins to rely on AI-generated presentations, there\\'s a risk that originality might be sacrificed, leading to a sea of \"cookie-cutter\" slides. Moreover, it raises concerns about whether the skill of visually crafting a narrative will diminish, much like how calculators altered the landscape of math education.\\n\\nSo, the question remains: will AI slide generators become a standard productivity tool, akin to templates of the past, or will human-crafted presentations continue to hold their place as the gold standard? As we stand at this crossroads, the decision lies in our hands, shaping the future of how we communicate and present our ideas.'}], count=None)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result = supabase\\\n", + " .table('blog_posts')\\\n", + " .select('''\n", + " id,\n", + " title,\n", + " author,\n", + " created_at,\n", + " tags,\n", + " content\n", + " ''')\\\n", + " .eq('published', True)\\\n", + " .order('created_at', desc=True)\\\n", + " .execute()\n", + "\n", + "result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4576957", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Local venv (Python)", + "language": "python", + "name": "localvenv" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/rd_pipeline_bdata.py b/backend/rd_pipeline_bdata.py new file mode 100644 index 0000000000000000000000000000000000000000..06f240efe0be273fd436036feb0d6ba02cc5a3f3 --- /dev/null +++ b/backend/rd_pipeline_bdata.py @@ -0,0 +1,130 @@ +from flexible_blog_database import FlexibleBlogDatabase +import os, time, logging, requests, json +from typing import List, Dict, Optional +from llm_agent import process_story +from brightdata_api import reddit_search_api, scrape_and_download_reddit +from supabase_api import insert_blog_post +from collections import OrderedDict +import datetime +from dotenv import load_dotenv +load_dotenv() + +logger = logging.getLogger("rd_pipeline_bdata") +logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO")) + +url_category_mapping = OrderedDict({ + "Artificial Intelligence": "https://www.reddit.com/r/ArtificialInteligence/", + "Social": "https://www.reddit.com/r/TrueOffMyChest/", + "Other": "https://www.reddit.com/r/relationship_advice/", + "Movies": "https://www.reddit.com/r/movies/", + "Other": "https://www.reddit.com/r/stories/", + "Developers": "https://www.reddit.com/r/developersIndia/", + "AI Agents": "https://www.reddit.com/r/aiagents/" +}) + +def scrape_and_download_reddit(url="https://www.reddit.com/r/ArtificialInteligence/"): + + reddit_response = reddit_search_api(url) + if not reddit_response or reddit_response.get("total_found", 0) == 0: + print("No posts found or error occurred during Reddit search.") + return None + + return reddit_response + +def find_best_post(posts_dict): + """Return post indexes in descending order based on scoring""" + posts_info = posts_dict + if not posts_info: + raise ValueError("No posts found from Reddit API.") + + # weight configuration (tweak as desired) + weights = { + "length": 0.3, # weight for length of post_content + "ups": 0.3, # weight for ups + "comments": 0.2, # weight for num_comments + "ratio": 0.2 # weight for upvote_ratio + } + + # calculate maxima for normalization + len_max = max(len(p["description"]) if p["description"] else 0 for p in posts_info) or 1 + ups_max = max(p["upvotes"] or 0 for p in posts_info) or 1 + comments_max = max(p["num_comments"] or 0 for p in posts_info) or 1 + + def score(post): + length_score = (len(post["description"]) if post["description"] else 0) / len_max + ups_score = (post["upvotes"] or 0) / ups_max + comments_score = (post["num_comments"] or 0) / comments_max + + return (weights["length"] * length_score + + weights["ups"] * ups_score + + weights["comments"] * comments_score) + + # Get scores for each post and sort indexes + scored_indexes = sorted( + range(len(posts_info)), + key=lambda idx: score(posts_info[idx]), + reverse=True + ) + + return scored_indexes + +def process_and_store_post(user_input=None, max_trials=5): + """ + Simplified + optimized: + - If user_input given, process it directly. + - Else fetch Reddit posts, try top candidates until one succeeds. + """ + if user_input: + raw_story = user_input + meta = {"title": "User Provided Story", "author": "anonymous"} + result = process_story(raw_story, enhanced=False) + else: + today = datetime.date.today() + weekday_python = today.weekday() + category_list = list(url_category_mapping.keys()) + category_index = weekday_python % len(category_list) + response_bd = scrape_and_download_reddit(url=url_category_mapping[category_list[category_index]]) + posts = response_bd['parsed_posts'] if response_bd else [] + if not posts: + logger.warning("No Reddit posts available after retries; aborting.") + return None + order = find_best_post(posts) + result = None + meta = None + for idx in order[:max_trials]: + post = posts[idx] + content = post.get("description") + if not content: + continue + try: + result = process_story(content, enhanced=False) + raw_story = content + meta = post + break + except Exception: + continue + if result is None or not meta: + logger.error("Could not process any candidate post.") + return None + + if not result or not meta: + return None + print(f"Story Preview:\n{result['polished_story'][:500]}...") + keywords = result.get("keywords") or [] + if keywords: + print("Keywords:", ", ".join(keywords)) + + write_data = { + "title": meta.get("title"), + "content": result.get("polished_story", ""), + "author": meta.get("user_posted"), + "tags": result.get("keywords", []), # Fixed: use .get() with default empty list + "created_at": meta.get("date_posted"), # Fixed: use date_posted instead of timestamp + "category": category_list[category_index] # Added category field + } + write_response = insert_blog_post(write_data) + reddit_done = f"Data written to Supabase with response: {write_response}" + return reddit_done + +if __name__ == "__main__": + process_and_store_post() \ No newline at end of file diff --git a/backend/rd_pipeline_local.py b/backend/rd_pipeline_local.py new file mode 100644 index 0000000000000000000000000000000000000000..6a0c1403027a4d5cc43d01c67dc1b9e271d66907 --- /dev/null +++ b/backend/rd_pipeline_local.py @@ -0,0 +1,194 @@ +from supabase_auth import datetime +from flexible_blog_database import FlexibleBlogDatabase +from supabase_api import insert_blog_post +import os, time, logging, requests, json +from typing import List, Dict, Optional +from llm_agent import process_story +from dotenv import load_dotenv +load_dotenv() + +logger = logging.getLogger("rd_pipeline") +logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO")) + +def fetch_reddit_posts(url: Optional[str] = None, attempts: int = 3, backoff_base: float = 2.0) -> List[Dict]: + """Fetch recent posts from Reddit with retry + fallback. + Returns list (may be empty). No exceptions bubble for HTTP/JSON failures. + """ + if not url: + url = "https://www.reddit.com/r/TrueOffMyChest/hot.json?limit=25&raw_json=1" + headers = { + "User-Agent": os.getenv( + "REDDIT_USER_AGENT", + "script:amplify.rd_pipeline:v1.0 (by u/exampleuser contact: noreply@example.com)" + ), + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "close" + } + last_err: Optional[Exception] = None + for attempt in range(1, attempts + 1): + try: + resp = requests.get(url, headers=headers, timeout=15) + status = resp.status_code + if status == 200: + try: + reddit_data = resp.json() + except ValueError as e: + last_err = e + logger.warning("JSON decode failed (attempt %s): %s", attempt, e) + else: + posts_info = [ + { + "title": c["data"].get("title"), + "post_content": c["data"].get("selftext"), + "author": c["data"].get("author"), + "upvote_ratio": c["data"].get("upvote_ratio"), + "ups": c["data"].get("ups"), + "num_comments": c["data"].get("num_comments"), + } + for c in reddit_data.get("data", {}).get("children", []) + ] + logger.info("Fetched %d Reddit posts", len(posts_info)) + return posts_info + elif status in (403, 429): + logger.warning("Reddit returned %s (attempt %s/%s)", status, attempt, attempts) + else: + logger.warning("Unexpected status %s (attempt %s/%s)", status, attempt, attempts) + time.sleep(backoff_base ** (attempt - 1)) + except (requests.RequestException, Exception) as e: + last_err = e + logger.warning("Error fetching Reddit posts (attempt %s/%s): %s", attempt, attempts, e) + time.sleep(backoff_base ** (attempt - 1)) + fallback_path = os.getenv("REDDIT_FALLBACK_JSON") + if fallback_path and os.path.isfile(fallback_path): + try: + with open(fallback_path, "r", encoding="utf-8") as f: + cached = json.load(f) + posts_info = [ + { + "title": c["data"].get("title"), + "post_content": c["data"].get("selftext"), + "author": c["data"].get("author"), + "upvote_ratio": c["data"].get("upvote_ratio"), + "ups": c["data"].get("ups"), + "num_comments": c["data"].get("num_comments"), + } + for c in cached.get("data", {}).get("children", []) + ] + logger.info("Loaded %d posts from fallback JSON", len(posts_info)) + return posts_info + except Exception as e: + logger.error("Failed reading fallback JSON: %s", e) + logger.error("All Reddit fetch attempts failed. Last error: %s", last_err) + return [] + +def find_best_post(posts_dict): + """Return post indexes in descending order based on scoring""" + posts_info = posts_dict + if not posts_info: + raise ValueError("No posts found from Reddit API.") + + # weight configuration (tweak as desired) + weights = { + "length": 0.3, # weight for length of post_content + "ups": 0.3, # weight for ups + "comments": 0.2, # weight for num_comments + "ratio": 0.2 # weight for upvote_ratio + } + + # calculate maxima for normalization + len_max = max(len(p["post_content"]) if p["post_content"] else 0 for p in posts_info) or 1 + ups_max = max(p["ups"] or 0 for p in posts_info) or 1 + comments_max = max(p["num_comments"] or 0 for p in posts_info) or 1 + + def score(post): + length_score = (len(post["post_content"]) if post["post_content"] else 0) / len_max + ups_score = (post["ups"] or 0) / ups_max + comments_score = (post["num_comments"] or 0) / comments_max + ratio_score = post["upvote_ratio"] or 0 + + return (weights["length"] * length_score + + weights["ups"] * ups_score + + weights["comments"] * comments_score + + weights["ratio"] * ratio_score) + + # Get scores for each post and sort indexes + scored_indexes = sorted( + range(len(posts_info)), + key=lambda idx: score(posts_info[idx]), + reverse=True + ) + + return scored_indexes + +def process_and_store_post(user_input=None, max_trials=5): + """ + Simplified + optimized: + - If user_input given, process it directly. + - Else fetch Reddit posts, try top candidates until one succeeds. + """ + if user_input: + raw_story = user_input + meta = {"title": "User Provided Story", "author": "anonymous"} + result = process_story(raw_story, enhanced=False) + else: + posts = fetch_reddit_posts() + if not posts: + logger.warning("No Reddit posts available after retries; aborting.") + return None + order = find_best_post(posts) + result = None + meta = None + for idx in order[:max_trials]: + post = posts[idx] + content = post.get("post_content") + if not content: + continue + try: + result = process_story(content, enhanced=False) + print(result) + raw_story = content + meta = post + break + except Exception as e: + print(f"Exception occurred : {str(e)}") + continue + if result is None or not meta: + logger.error("Could not process any candidate post.") + return None + + if not result or not meta: + return None + print(f"Story Preview:\n{result['polished_story'][:500]}...") + keywords = result.get("keywords") or [] + if keywords: + print("Keywords:", ", ".join(keywords)) + + from datetime import datetime + write_data = { + "title": meta.get("title"), + "content": result.get("polished_story", ""), + "author": meta.get("author", "unknown"), + "tags": result.get("keywords", []), # Fixed: use .get() with default empty list + "created_at": meta.get("date_posted", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) # Fixed: use date_posted instead of timestamp + } + #print(f"Write Data : {write_data}") + # print("==========================") + # print(f"Here are the meta details : {meta}") + # print("==========================") + # print(f"Here is the write data : {write_data}") + # print("==========================") + write_response = insert_blog_post(write_data) + reddit_done = f"Data written to Supabase with response: {write_response}" + + # blog_db = FlexibleBlogDatabase() + # blog_id = blog_db.create_blog_post( + # title=meta.get("title") or "Untitled", + # content=result['polished_story'], + # author=meta.get("author") or "unknown", + # tags=keywords + # ) + return reddit_done + +if __name__ == "__main__": + process_and_store_post() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..9117d543b057f11aeda2def1ad3d73ab7db27575 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,14 @@ +fastapi +uvicorn +langchain-community>=0.3.27 +pyyaml +pydantic>=2.0 +python-dotenv +langchain>=0.2.0 +langchain-openai>=0.1.0 +openai>=1.30.0 +langgraph +langchain-mcp-adapters +fastapi-mcp +supabase +pip-system-certs \ No newline at end of file diff --git a/backend/supabase_api.ipynb b/backend/supabase_api.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/supabase_api.py b/backend/supabase_api.py new file mode 100644 index 0000000000000000000000000000000000000000..e626c183e60aa7073646824b71b57240d1fd75e6 --- /dev/null +++ b/backend/supabase_api.py @@ -0,0 +1,24 @@ +from supabase import create_client, Client +import os +from dotenv import load_dotenv +load_dotenv() + +url = os.environ.get("SUPABASE_URL", "") +key = os.environ.get("SUPABASE_KEY", "") +supabase: Client = create_client(url, key) + +def insert_blog_post(data: dict): + try: + response = supabase.table("blog_posts").insert(data).execute() + print("Data inserted successfully:", response) + except Exception as e: + print("Error inserting data:", e) + +def fetch_reddit_data(): + try: + response = supabase.table("blog_posts").select("*").execute() + print("Data fetched successfully:", response.data) + return response.data + except Exception as e: + print("Error fetching data:", e) + return None \ No newline at end of file diff --git a/builderflow.md b/builderflow.md new file mode 100644 index 0000000000000000000000000000000000000000..735e0623ba6d7af5a38ef6ea81385e534d1b17a3 --- /dev/null +++ b/builderflow.md @@ -0,0 +1,180 @@ +# Commit-1 + +# ReactFast Project Overview + +A minimal full-stack setup with a FastAPI backend serving a Vite + React frontend. The frontend builds into `frontend/dist`, and FastAPI mounts it under the `/app` route. + +## What this project includes +- Backend: FastAPI app (`backend/app.py`) mounting static files from the React build. +- Frontend: Vite + React (TypeScript) app configured with base `/app/` so assets resolve when hosted under that path. +- Local dev: Build frontend once, then run the FastAPI server. Visit `http://127.0.0.1:/app/`. + +## Dependencies +- Python (backend) + - fastapi: Web framework serving API and static files + - uvicorn: ASGI server to run the FastAPI app +- Node (frontend) + - react, react-dom: UI library and DOM renderer + - vite: Build tool and dev server + - @vitejs/plugin-react: React plugin for Vite (Fast Refresh, JSX, etc.) + - typescript, @types/react, @types/react-dom: TypeScript and React typings + +--- + +## Folder tree and file descriptions + +### backend/ +``` +backend/ +β”œβ”€ app.py # FastAPI app mounting the React build at /app +β”œβ”€ requirements.txt # Python dependencies for backend (fastapi, uvicorn) +β”œβ”€ __pycache__/ # Python bytecode cache (auto-generated) +└─ .venv/ # Local Python virtual environment (developer local) +``` + +### frontend/ +``` +frontend/ +β”œβ”€ index.html # Vite HTML entry; loads /src/main.tsx +β”œβ”€ package.json # Frontend scripts and dependencies +β”œβ”€ package-lock.json # Exact dependency versions (npm lockfile) +β”œβ”€ tsconfig.json # TypeScript compiler options for the app +β”œβ”€ vite.config.ts # Vite config; base set to /app and outDir=dist +β”œβ”€ src/ # Application source code +β”‚ β”œβ”€ App.tsx # Main UI component rendered by the app +β”‚ β”œβ”€ main.tsx # React entry; creates root and renders +β”‚ └─ style.css # Minimal global styles +β”œβ”€ dist/ # Production build output (generated by `npm run build`) +β”‚ β”œβ”€ index.html # Built HTML referencing hashed asset files under /app +β”‚ └─ assets/ # Hashed JS/CSS bundles and sourcemaps +β”‚ β”œβ”€ index-*.js # Production JS bundle (hashed filename) +β”‚ β”œβ”€ index-*.js.map # Sourcemap for debugging (if enabled) +β”‚ └─ index-*.css # Production CSS bundle (hashed filename) +└─ node_modules/ # Installed frontend dependencies (generated by npm) +``` + +--- + +## How it works +1. Build the frontend (Vite) which outputs to `frontend/dist` with asset URLs prefixed by `/app/`. +2. Start the FastAPI server; it mounts `frontend/dist` as static files at the `/app` route. +3. Navigate to `http://127.0.0.1:/app/` to view the app (index.html + assets). + +## Common commands (optional) +- Build frontend: `npm run build` in `frontend/` +- Run backend: `uvicorn app:app --host 127.0.0.1 --port 8000` in `backend/` (after installing requirements) + +## Notes +- If you change the frontend base path or output folder, update either Vite’s `base`/`build.outDir` or the backend static mount path accordingly. +- `dist/` is generatedβ€”do not edit files there manually; edit files under `src/` instead and rebuild. + +--- + +# Commit-2 + +High-level summary of enabling frontend ↔ backend communication. + +- Backend + - Added a simple POST API at `/api/transform` that accepts `{ text: string }` and returns `{ result: string }` with a minimal transformation. + - Kept the React static site mounted at `/app` so built assets resolve correctly (aligned with Vite `base: '/app/'`). + +- Frontend + - Updated the main UI (`src/App.tsx`) to include: + - A label, a textbox for user input, and a submit button. + - On submit, a `fetch('/api/transform', { method: 'POST', body: JSON.stringify({ text }) })` call. + - Displays the returned `result` string below the form. + - Light, elegant styling in `src/style.css` to keep the layout centered and readable without overengineering. + +- Result + - Users can type a message, submit, and see a transformed response from the FastAPI backendβ€”served together under the same origin, avoiding CORS configuration. + +--- + +# Commit-3 + +High-level summary of adding containerization (Docker) support. + +- Purpose + - Provide a reproducible build artifact bundling backend (FastAPI) and pre-built frontend (Vite) into one image. + - Simplify deployment: single `docker run` serves both API and static UI. + +- Dockerfile Structure (multi-stage) + - Stage 1 (node:20-alpine): installs frontend deps and runs `npm run build` to produce `dist/`. + - Stage 2 (python:3.12-slim): installs backend Python deps, copies backend code and built `frontend/dist`. + - Starts with: `uvicorn backend.app:app --host 0.0.0.0 --port 8000`. + +- Key Paths Inside Image + - `/app/backend` – FastAPI code + - `/app/frontend/dist` – Built static assets served by FastAPI at `/app` route + +- Added Files + - `Dockerfile` – Multi-stage build definition + - `.dockerignore` – Excludes node_modules, virtual envs, caches, VCS metadata, logs, etc., reducing context size and image bloat + +- Build & Run (local) + 1. Build image: + - `docker build -t reactfast .` + 2. Run container: + - `docker run --rm -p 8000:8000 reactfast` + 3. Access UI: + - `http://localhost:8000/app/` + +- Customization Notes + - To enable auto-reload in development, run locally without Docker or create a dev Dockerfile variant mounting source. + - For production scaling, consider adding a process manager (e.g., `gunicorn` with `uvicorn.workers.UvicornWorker`) and HEALTHCHECK. + - Pin dependency versions more strictly if reproducibility across time is critical. + +- Outcome + - Project can be built and deployed as a single immutable image; frontend and backend remain in sync at build time. + +- Pushing the app to Azure Container Registry. Use below commands + - `docker login` to login to Azure Container Registry + - `docker tag :latest .azurecr.io/:latest` + - `docker push .azurecr.io/:latest` + + + # Commit-4 + + High-level summary of adding CI automation (GitHub Actions) to build and push the Docker image to Azure Container Registry (ACR). + + - Purpose + - Automate image builds on each push to `main` (and manual dispatch) ensuring the registry always has an up‑to‑date image. + - Provide traceable image tags (`` and `latest`) for rollback and promotion. + + - Secrets / Inputs + - `AZURE_CREDENTIALS`: JSON from `az ad sp create-for-rbac --role AcrPush --scopes --sdk-auth`. + - `ACR_LOGIN_SERVER`: e.g. `minimum.azurecr.io`. + - (Optional) `ACR_NAME` if deriving login server dynamically. + + - Workflow Steps (simplified) + 1. Checkout repository source. + 2. Azure login using service principal (`azure/login`). + 3. Authenticate to ACR (either via `az acr login` or `docker/login-action`). + 4. Build Docker image with existing multi-stage `Dockerfile`. + 5. Tag image twice: `:` and `:latest`. + 6. Push both tags to ACR. + 7. Summarize pushed tags for visibility. + + - Tagging Strategy + - Immutable: `registry/app:{{ github.sha }}` for precise traceability. + - Mutable convenience: `registry/app:latest` for default deployments / quick tests. + + - Minimal Example (conceptual) + - Trigger: `on: push: branches: [ main ]` + `workflow_dispatch`. + - Uses official actions: `actions/checkout`, `azure/login`, `docker/build-push-action`. + + - Benefits + - Eliminates manual local build/push steps. + - Reduces risk of β€œworks on my machine” discrepancies. + - Provides consistent, auditable artifact generation tied to commit history. + + - Follow-on Opportunities + - Add deploy job (e.g., to Azure Web App / Container Apps / AKS) after successful push. + - Introduce image security scanning (Trivy / Microsoft Defender). + - Add build cache (GitHub Actions cache or ACR build tasks) for faster builds. + - Add semantic version tagging (e.g., `v1.2.3`) if release process formalizes. + + - Outcome + - CI pipeline ensures every code change can rapidly produce a runnable, versioned container image in ACR, ready for deployment workflows. + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b553cdae85022c2bf02205b9d91a9009e72a4694 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + ReactFast + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..6c10c12b4554daf727bebd6b3718b88deb18669f --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1658 @@ +{ + "name": "reactfast-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reactfast-frontend", + "version": "0.0.1", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "typescript": "^5.5.4", + "vite": "^5.4.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", + "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz", + "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz", + "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz", + "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz", + "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz", + "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz", + "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz", + "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz", + "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz", + "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz", + "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz", + "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz", + "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz", + "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz", + "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz", + "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz", + "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz", + "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz", + "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz", + "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz", + "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.213", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", + "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", + "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.0", + "@rollup/rollup-android-arm64": "4.50.0", + "@rollup/rollup-darwin-arm64": "4.50.0", + "@rollup/rollup-darwin-x64": "4.50.0", + "@rollup/rollup-freebsd-arm64": "4.50.0", + "@rollup/rollup-freebsd-x64": "4.50.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", + "@rollup/rollup-linux-arm-musleabihf": "4.50.0", + "@rollup/rollup-linux-arm64-gnu": "4.50.0", + "@rollup/rollup-linux-arm64-musl": "4.50.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", + "@rollup/rollup-linux-ppc64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-musl": "4.50.0", + "@rollup/rollup-linux-s390x-gnu": "4.50.0", + "@rollup/rollup-linux-x64-gnu": "4.50.0", + "@rollup/rollup-linux-x64-musl": "4.50.0", + "@rollup/rollup-openharmony-arm64": "4.50.0", + "@rollup/rollup-win32-arm64-msvc": "4.50.0", + "@rollup/rollup-win32-ia32-msvc": "4.50.0", + "@rollup/rollup-win32-x64-msvc": "4.50.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..97f0207eb1b1cf75d3e10fee50e9000d85fcf28a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "reactfast-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --port 5173" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.0", + "typescript": "^5.5.4", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c1ec06f41e5a5c93c8adce545bcd479dc574bf92 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,452 @@ +import { useState, useEffect, type FormEvent } from 'react'; + +interface BlogPost { + id: number; + title: string; + content: string; + author: string; + created_at: string; + published: boolean; + tags: string[]; + featured_image?: { + url: string; + alt_text: string; + caption: string; + }; + post_images: Array<{ + id: number; + url: string; + alt_text: string; + caption: string; + order: number; + position?: number; + }>; +} + +interface BlogSummary { + id: number; + title: string; + author: string; + created_at: string; + tags: string[]; + excerpt: string; + has_featured_image: boolean; + featured_image_url?: string; + post_image_count: number; +} + +interface BlogResponse { + posts: BlogSummary[]; + total: number; + limit: number; + offset: number; + has_more: boolean; +} + +export default function App() { + const [blogData, setBlogData] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + const [viewMode, setViewMode] = useState<'home' | 'blog'>('home'); + const [currentPage, setCurrentPage] = useState(1); + const [isDragging, setIsDragging] = useState(false); + const [headerCollapsed, setHeaderCollapsed] = useState(false); + const [lastScrollY, setLastScrollY] = useState(0); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('All'); + const [isLoading, setIsLoading] = useState(false); + const [searchResults, setSearchResults] = useState(null); + const [isSearching, setIsSearching] = useState(false); + const [searchTimer, setSearchTimer] = useState(null); + + const PAGE_SIZE = 6; + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && (!blogData || newPage <= Math.ceil(blogData.total / PAGE_SIZE))) { + setCurrentPage(newPage); + fetchBlogPosts(newPage); + } + }; + const [sliderVisible, setSliderVisible] = useState(false); + + const categories = ['All', 'Artificial Intelligence','Developers','AI Agents','Social','Movies']; + + // Fetch blog posts on component mount and page change (disabled when searching) + useEffect(() => { + if (!searchQuery) fetchBlogPosts(currentPage); + }, [currentPage, searchQuery, selectedCategory]); + + // When category changes, reset page & clear search results (if any) + useEffect(() => { + setCurrentPage(1); + if (!searchQuery) { + fetchBlogPosts(1); + } + }, [selectedCategory]); + + // Handle scroll effect for blog header + useEffect(() => { + if (viewMode !== 'blog') return; + + const handleScroll = () => { + const currentScrollY = window.scrollY; + const scrollThreshold = 100; // Minimum scroll distance to trigger effect + + if (currentScrollY > scrollThreshold) { + // Scrolling down - collapse header + if (currentScrollY > lastScrollY && !headerCollapsed) { + setHeaderCollapsed(true); + } + // Scrolling up - expand header + else if (currentScrollY < lastScrollY && headerCollapsed) { + setHeaderCollapsed(false); + } + } else { + // Near top - always show full header + setHeaderCollapsed(false); + } + + setLastScrollY(currentScrollY); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, [viewMode, lastScrollY, headerCollapsed]); + + async function fetchBlogPosts(page: number = 1) { + setIsLoading(true); + try { + const params = new URLSearchParams({ page: String(page), limit: String(PAGE_SIZE) }); + if (selectedCategory && selectedCategory !== 'All') params.append('category', selectedCategory); + const res = await fetch(`/api/blog/posts?${params.toString()}`); + if (res.ok) { + const data = await res.json(); + // Small delay to make loading visible + await new Promise(resolve => setTimeout(resolve, 300)); + setBlogData(data); + } + } catch (err) { + console.error('Failed to fetch blog posts:', err); + } finally { + setIsLoading(false); + } + } + + async function runSearch(query: string) { + const q = query.trim(); + if (!q) { + setSearchResults(null); + return; + } + setIsSearching(true); + try { + const params = new URLSearchParams({ q }); + if (selectedCategory && selectedCategory !== 'All') params.append('category', selectedCategory); + const res = await fetch(`/api/blog/search?${params.toString()}`); + if (res.ok) { + const data = await res.json(); + setSearchResults({ + posts: data.posts, + total: data.total, + limit: data.posts.length, + offset: 0, + has_more: false + } as BlogResponse); + } + } catch (e) { + console.error('Search failed', e); + } finally { + setIsSearching(false); + } + } + + // Debounce search input + useEffect(() => { + if (searchTimer) window.clearTimeout(searchTimer); + const handle = window.setTimeout(() => { + runSearch(searchQuery); + }, 300); + setSearchTimer(handle); + return () => window.clearTimeout(handle); + }, [searchQuery]); + + async function fetchBlogPost(id: number) { + setIsLoading(true); + try { + const res = await fetch(`/api/blog/posts/${id}`); + if (res.ok) { + const post = await res.json(); + // Add artificial delay to make loading visible + await new Promise(resolve => setTimeout(resolve, 300)); + setSelectedPost(post); + setViewMode('blog'); + } + } catch (err) { + console.error('Failed to fetch blog post:', err); + } finally { + setIsLoading(false); + } + } + + function formatDate(dateString: string) { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + + function renderBlogContent(content: string, images: BlogPost['post_images']) { + const paragraphs = content.split('\n\n').filter(p => p.trim()); + const elements: JSX.Element[] = []; + + paragraphs.forEach((paragraph, index) => { + const paragraphNumber = index + 1; + + elements.push( +

+ {paragraph} +

+ ); + + // Insert images that should appear after this paragraph + const imagesForPosition = images.filter(img => img.position === paragraphNumber); + imagesForPosition.forEach(image => { + elements.push( +
+ {image.alt_text} + {image.caption &&
{image.caption}
} +
+ ); + }); + }); + + return elements; + } + + // Toggle subtle separator only if content is scrollable or user scrolled + useEffect(() => { + function evaluate() { + const header = document.querySelector('.compact-header'); + if (!header) return; + const scrollable = document.documentElement.scrollHeight > window.innerHeight + 4; + const scrolled = window.scrollY > 4; + if (scrollable || scrolled) header.classList.add('with-sep'); + else header.classList.remove('with-sep'); + } + evaluate(); + window.addEventListener('resize', evaluate); + window.addEventListener('scroll', evaluate, { passive: true }); + return () => { + window.removeEventListener('resize', evaluate); + window.removeEventListener('scroll', evaluate); + }; + }, []); + + if (viewMode === 'blog' && selectedPost) { + return ( +
+ + ); + } + + return ( +
+ + ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e92e220a0de0ab8684840dbee534adc5530ebf9d --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './style.css' + +const el = document.getElementById('root') +if (el) { + createRoot(el).render( + + + + ) +} diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000000000000000000000000000000000000..3596207d1ffd8eed3e57c3cb2a2f8b2522c2fa6f --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,1029 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap'); + +* { box-sizing: border-box; } +html, body, #root { height: 100%; } +body { margin:0; font-family:'Inter',system-ui,sans-serif; color:#1b2126; background:#f2f4f8; overflow-x:hidden; -webkit-font-smoothing:antialiased; } + +.app-root { position:relative; min-height:100%; padding-top:150px; } + +/* Layered subtle geometric background */ +.bg-layers { position:fixed; inset:0; pointer-events:none; z-index:0; background: + linear-gradient(145deg, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0) 65%), + radial-gradient(circle at 78% 24%, rgba(96,165,250,0.18), transparent 60%), + radial-gradient(circle at 15% 70%, rgba(167,139,250,0.15), transparent 62%), + radial-gradient(circle at 50% 85%, rgba(125,168,255,0.12), transparent 58%), + repeating-linear-gradient(115deg, rgba(0,0,0,0.025) 0 14px, rgba(0,0,0,0) 14px 28px), + linear-gradient(180deg,#f3f5f9,#eef1f6); + mask: linear-gradient(#fff,rgba(255,255,255,0.35)); } + +/* Elevated header */ +.top-bar.improved { position:fixed; top:0; left:0; right:0; display:flex; justify-content:center; padding:30px 32px 18px; z-index:10; -webkit-backdrop-filter:blur(20px) saturate(210%); backdrop-filter:blur(20px) saturate(210%); background:rgba(255,255,255,0.75); /* separator removed by default */ } +.top-bar.improved.with-sep { border-bottom:1px solid rgba(0,0,0,0.05); box-shadow:0 14px 48px -24px rgba(0,0,0,0.35), 0 4px 14px -8px rgba(0,0,0,0.1); } +.bar-inner { width:80%; max-width:1180px; display:flex; flex-direction:column; gap:18px; } +.title-row { display:flex; align-items:baseline; gap:18px; flex-wrap:wrap; } +.app-title { margin:0; font-size:1.55rem; letter-spacing:-0.5px; font-weight:640; display:flex; align-items:center; gap:6px; background:linear-gradient(90deg,#1b2735,#3b5168); -webkit-background-clip:text; background-clip:text; color:transparent; } +.pulse-dot { width:10px; height:10px; border-radius:50%; background:linear-gradient(135deg,#60a5fa,#818cf8); position:relative; box-shadow:0 0 0 0 rgba(96,165,250,0.55); animation:pulse 3s ease-in-out infinite; } +@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(96,165,250,0.55);} 55%{box-shadow:0 0 0 10px rgba(96,165,250,0);} 100%{box-shadow:0 0 0 0 rgba(96,165,250,0);} } +.tagline { font-size:.78rem; font-weight:500; color:#5d6670; letter-spacing:.55px; } + +/* Grid Loading */ +.grid-loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 2rem; + margin: 2rem auto; + text-align: center; +} + +.grid-loading p { + color: #6366f1; + font-size: 0.9rem; + font-weight: 500; + margin: 0; +} + +.match-badge { + background: linear-gradient(135deg,#6366f1,#8b5cf6); + color:#fff; + font-size:0.65rem; + padding:4px 8px; + border-radius: 999px; + font-weight:600; + letter-spacing:.5px; + box-shadow:0 2px 6px rgba(0,0,0,0.18); + display:inline-flex; + align-items:center; + gap:4px; + margin-left:6px; +} + +/* Input form */ +.input-form { display:flex; width:100%; gap:14px; align-items:stretch; } +.input-form.fancy .input-shell { position:relative; flex:1; display:flex; align-items:stretch; } +.input-shell .accent-bar { position:absolute; left:12px; top:12px; bottom:12px; width:4px; border-radius:3px; background:linear-gradient(180deg,#60a5fa,#818cf8); opacity:.35; transition:opacity .35s, filter .35s; } +.input-shell:focus-within .accent-bar { opacity:1; filter:saturate(150%); } +.big-input { flex:1; min-height:70px; padding:20px 30px 20px 30px; font-size:1.02rem; line-height:1.35; border-radius:18px; border:1px solid #cdd3d9; background:linear-gradient(180deg,#ffffff,#f8fafc); color:#192027; outline:none; box-shadow:0 1px 3px rgba(0,0,0,0.05); transition:border-color .25s, box-shadow .35s, background .35s; } +.big-input::placeholder { color:#9da4ad; } +.big-input:focus { border-color:#7da8ff; box-shadow:0 0 0 3px rgba(125,168,255,0.26), 0 6px 22px -10px rgba(125,168,255,0.45); background:#ffffff; } + +.submit-btn { padding:0 34px; font-size:0.92rem; font-weight:600; border:1px solid #b9c2cc; border-radius:18px; background:linear-gradient(135deg,#6da8ff,#818cf8); color:#fff; cursor:pointer; letter-spacing:.45px; display:flex; align-items:center; justify-content:center; box-shadow:0 6px 24px -10px rgba(109,168,255,0.65),0 3px 10px -6px rgba(0,0,0,0.25); transition:transform .3s, box-shadow .35s, filter .35s, background-position .5s; min-width:132px; background-size:200% 200%; background-position:15% 35%; } +.submit-btn:disabled { opacity:.5; cursor:not-allowed; filter:grayscale(.35); } +.submit-btn:not(:disabled):hover { transform:translateY(-3px); background-position:55% 65%; box-shadow:0 14px 40px -16px rgba(109,168,255,0.7),0 6px 16px -10px rgba(0,0,0,0.22); } +.submit-btn:not(:disabled):active { transform:translateY(1px); box-shadow:0 6px 18px -10px rgba(109,168,255,0.6); } + +/* Response area */ +/* Response / output */ +.response-area { position:relative; z-index:1; display:flex; flex-direction:column; gap:26px; align-items:center; padding:10px 34px 90px; margin-top:16px; } + +.card { width:80%; max-width:1180px; background:linear-gradient(170deg,#ffffff,#f5f7fa); border:1px solid #d5dbe1; -webkit-backdrop-filter:blur(12px) saturate(170%); backdrop-filter:blur(12px) saturate(170%); border-radius:22px; padding:34px 42px 40px; color:#20262b; box-shadow:0 18px 46px -26px rgba(0,0,0,0.28), 0 8px 20px -14px rgba(0,0,0,0.15); display:flex; flex-direction:column; gap:14px; animation:fadeIn .6s ease; position:relative; overflow:hidden; } +.card:before { content:""; position:absolute; inset:0; background:radial-gradient(circle at 85% 20%, rgba(129,140,248,0.14), transparent 60%), radial-gradient(circle at 12% 82%, rgba(96,165,250,0.14), transparent 55%); pointer-events:none; } +.card-label { font-size:.64rem; letter-spacing:1.3px; text-transform:uppercase; opacity:.55; font-weight:600; color:#5c6670; } +.card-content { font-size:1.05rem; line-height:1.58; white-space:pre-wrap; word-break:break-word; } +.card-result { border-left:6px solid #91b6ff; margin-top: 10px;} +.card-error { border-left:6px solid #ef7d7d; background:linear-gradient(165deg,#fff5f5,#ffecec); } +.placeholder-hint { width:80%; max-width:1180px; font-size:.8rem; color:#69727b; text-align:left; padding:4px 6px 0; font-style:italic; } + +@media (max-width: 1100px) { .bar-inner, .card, .placeholder-hint { width:86%; } .big-input { min-height:64px; } } +@media (max-width: 900px) { .bar-inner, .card, .placeholder-hint { width:90%; } .app-root{padding-top:140px;} .big-input{min-height:60px; padding:18px 24px;} .submit-btn{min-width:116px;} .card{padding:30px 34px 34px;} } + +@media (max-width: 640px) { .input-form{flex-direction:column;} .submit-btn{width:100%; height:60px;} .big-input{border-radius:18px; min-height:62px;} .bar-inner,.card,.placeholder-hint{width:94%;} .app-root{padding-top:152px;} } + +@keyframes fadeIn { from { opacity: 0; transform: translateY(6px);} to { opacity:1; transform: translateY(0);} } + +/* Scrollbar subtle styling */ +::-webkit-scrollbar { width: 10px; } +::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); } +::-webkit-scrollbar-thumb { background: linear-gradient(#334155,#1e293b); border-radius: 20px; } +::-webkit-scrollbar-thumb:hover { background: linear-gradient(#475569,#334155); } + +/* New Homepage Layout Styles */ +/* Layout base spacing adjusted for larger header */ +.homepage-layout { padding-top: 0; } + +/* New Enlarged Header */ +.site-header { position:sticky; top:0; z-index:120; background:linear-gradient(125deg,#ffffffcc 0%,#f8fafccc 65%,#eef2ffcc 100%); backdrop-filter:blur(26px) saturate(180%); -webkit-backdrop-filter:blur(26px) saturate(180%); border-bottom:1px solid rgba(99,102,241,0.1); box-shadow:0 4px 24px -10px rgba(99,102,241,0.25), 0 2px 8px -4px rgba(0,0,0,0.08); } +.site-header:before { content:""; position:absolute; inset:0; pointer-events:none; background:radial-gradient(circle at 6% 90%, rgba(129,140,248,0.20), transparent 60%), radial-gradient(circle at 95% 15%, rgba(96,165,250,0.25), transparent 55%); mix-blend-mode:overlay; opacity:.6; } +.header-inner { max-width:1320px; margin:0 auto; padding:34px clamp(2rem,7vw,7rem) 30px; display:flex; gap:40px; align-items:flex-end; flex-wrap:wrap; justify-content:space-between; position:relative; } +.brand-block { display:flex; flex-direction:column; gap:10px; min-width:260px; } +.site-title { margin:0; font-size:clamp(2rem,3.2vw,3.05rem); font-weight:700; letter-spacing:-1px; line-height:1; background:linear-gradient(90deg,#1e293b,#334155 40%,#4c1d95 80%); -webkit-background-clip:text; background-clip:text; color:transparent; display:inline-flex; align-items:center; gap:10px; } +.site-title .pulse-dot { width:14px; height:14px; background:linear-gradient(135deg,#6366f1,#8b5cf6); box-shadow:0 0 0 0 rgba(99,102,241,0.55); animation:pulse 3s ease-in-out infinite; } +.site-tagline { margin:0; font-size:0.9rem; font-weight:500; letter-spacing:0.4px; color:#475569; max-width:520px; line-height:1.4; } +.header-controls { display:flex; gap:18px; align-items:center; margin-left:auto; flex-wrap:wrap; } +.search-wrapper, .category-wrapper { position:relative; } +.header-search { width:300px; background:linear-gradient(180deg,#ffffff,#f1f5f9); border:1.5px solid #cbd5e1; padding:14px 18px 14px 48px; border-radius:18px; font-size:0.95rem; transition:all .35s; box-shadow:0 2px 6px rgba(0,0,0,0.05); } +.header-search:focus { outline:none; border-color:#6366f1; box-shadow:0 0 0 4px rgba(99,102,241,0.18), 0 6px 22px -8px rgba(99,102,241,0.35); background:#ffffff; } +.header-search + .search-icon { position:absolute; left:18px; top:50%; transform:translateY(-50%); font-size:1rem; color:#64748b; } +.header-category { background:linear-gradient(180deg,#ffffff,#f1f5f9); border:1.5px solid #cbd5e1; padding:14px 46px 14px 18px; border-radius:18px; min-width:190px; font-size:0.9rem; cursor:pointer; transition:all .35s; appearance:none; } +.header-category:focus { outline:none; border-color:#6366f1; box-shadow:0 0 0 4px rgba(99,102,241,0.18), 0 6px 22px -8px rgba(99,102,241,0.35); background:#ffffff; } +.category-wrapper .dropdown-arrow { position:absolute; right:18px; top:50%; transform:translateY(-50%); font-size:0.75rem; color:#64748b; pointer-events:none; } + +@media (max-width:1000px){ + .header-inner { padding:26px clamp(1.75rem,5vw,4rem) 24px; } + .site-title { font-size:clamp(1.9rem,5vw,2.6rem); } + .header-search { width:240px; } +} +@media (max-width:780px){ + .header-inner { align-items:flex-start; } + .header-controls { width:100%; order:3; justify-content:flex-start; } + .header-search { width:100%; } + .brand-block { width:100%; } +} +@media (max-width:520px){ + .site-title { font-size:2.1rem; } + .site-tagline { font-size:0.8rem; } + .header-category { width:100%; } +} + +/* Remove old main-header styles */ +.main-header { + display: none; +} + +/* Removed old content-controls & latest-posts label (merged into site-header) */ + +.search-container { + position: relative; + display: flex; + align-items: center; +} + +.search-input { + padding: 12px 16px 12px 44px; + border: 2px solid #e2e8f0; + border-radius: 12px; + font-size: 0.95rem; + width: 280px; + background: white; + transition: all 0.3s ease; + outline: none; +} + +.search-input:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.search-icon { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: #94a3b8; + font-size: 1rem; + pointer-events: none; +} + +.category-dropdown { + position: relative; + display: flex; + align-items: center; +} + +.category-select { + padding: 12px 40px 12px 16px; + border: 2px solid #e2e8f0; + border-radius: 12px; + font-size: 0.95rem; + background: white; + cursor: pointer; + outline: none; + appearance: none; + min-width: 180px; + transition: all 0.3s ease; +} + +.category-select:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.dropdown-arrow { + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + color: #94a3b8; + font-size: 0.8rem; + pointer-events: none; +} + +/* Main Content Area */ +.main-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 32px 80px; +} + +/* Blog Content Section */ +.blog-content-section { + margin: 20px 0; + /* Debug: Ensure changes are applied */ + background: transparent; +} + +.blog-container { + width: 100%; +} + +/* Blog Grid Compact */ +.blog-grid-new { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:26px; margin-bottom:54px; } + +/* Card aesthetic refresh: compact & elevated */ +.blog-card-new { background:#ffffff; border-radius:18px; overflow:hidden; box-shadow:0 10px 28px -14px rgba(0,0,0,0.25), 0 4px 14px -6px rgba(0,0,0,0.12); transition:all .4s cubic-bezier(.4,.2,.2,1); cursor:pointer; border:1px solid #e2e8f0; position:relative; isolation:isolate; display:flex; flex-direction:column; } +.blog-card-new:before { content:""; position:absolute; inset:0; background:linear-gradient(140deg,rgba(99,102,241,0.07),rgba(56,189,248,0.05) 35%,rgba(255,255,255,0) 75%); opacity:0; transition:opacity .5s; pointer-events:none; } +.blog-card-new:hover:before { opacity:1; } + +.blog-card-new:hover { transform:translateY(-6px) scale(1.015); box-shadow:0 30px 60px -24px rgba(30,41,59,0.38), 0 18px 28px -12px rgba(30,41,59,0.22); border-color:#c7d2fe; } + +/* Media ratio refined */ +.blog-card-image-new { position:relative; width:100%; aspect-ratio:16/9; overflow:hidden; background:linear-gradient(135deg,#f8fafc,#e2e8f0); } + +.blog-card-image-new img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.blog-card-new:hover .blog-card-image-new img { + transform: scale(1.05); +} + +.image-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.1) 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.blog-card-new:hover .image-overlay { + opacity: 1; +} + +.blog-card-content-new { padding:20px 20px 22px; background:#ffffff; flex:1; display:flex; flex-direction:column; } + +.blog-card-tags-new { display:flex; gap:6px; margin-bottom:12px; flex-wrap:wrap; } + +.blog-card-tag-new { background:linear-gradient(135deg,#eef2ff,#e0e7ff); color:#4338ca; font-size:0.63rem; font-weight:600; padding:5px 10px; border-radius:14px; letter-spacing:.6px; text-transform:uppercase; } + +.blog-card-title-new { font-size:1.05rem; font-weight:650; color:#1e293b; margin:0 0 8px 0; line-height:1.35; letter-spacing:-0.3px; display:-webkit-box; -webkit-line-clamp:2; line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; } + +.blog-card-excerpt-new { font-size:0.8rem; color:#475569; line-height:1.5; margin:0 0 14px 0; display:-webkit-box; -webkit-line-clamp:3; line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; } + +.blog-card-meta-new { display:flex; justify-content:space-between; align-items:center; margin-top:auto; padding-top:12px; border-top:1px solid #f1f5f9; gap:10px; } + +.blog-card-author-new { font-size:0.65rem; font-weight:600; color:#334155; letter-spacing:.5px; text-transform:uppercase; } + +.blog-card-date-new { font-size:0.6rem; color:#64748b; letter-spacing:.5px; } + +.blog-card-stats-new { + display: flex; + align-items: center; + gap: 16px; +} + +.blog-card-stat-new { + font-size: 0.75rem; + color: #64748b; + background: #f8fafc; + padding: 4px 8px; + border-radius: 6px; +} + +/* Pagination */ +.pagination-new { + display: flex; + justify-content: center; + align-items: center; + gap: 24px; + margin-top: 48px; +} + +.pagination-btn-new { + padding: 12px 24px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.pagination-btn-new:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3); +} + +.pagination-btn-new:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #e2e8f0; + color: #94a3b8; +} + +.page-indicators { + display: flex; + gap: 8px; +} + +.page-indicator { + width: 40px; + height: 40px; + border-radius: 8px; + border: 2px solid #e2e8f0; + background: white; + color: #64748b; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.page-indicator:hover { + border-color: #667eea; + color: #667eea; +} + +.page-indicator.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: transparent; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .blog-grid-new { + grid-template-columns: 1fr; + gap: 24px; + } + + .controls-row { + flex-direction: column; + align-items: stretch; + gap: 16px; + } + + .controls-group { + justify-content: center; + } + + .search-input { + width: 100%; + max-width: 280px; + } +} + +@media (max-width: 768px) { + .homepage-layout { + padding-top: 70px; + } + + .compact-header .header-content { + padding: 12px 20px; + } + + .brand-section { + flex-direction: column; + gap: 4px; + } + + .homepage-layout .app-title { + font-size: 1.4rem; + } + + .homepage-layout .tagline { + font-size: 0.8rem; + } + + .controls-inner, .main-content { + padding: 0 20px; + } + + .controls-group { + flex-direction: column; + gap: 12px; + width: 100%; + } + + .search-input, .category-select { + width: 100%; + max-width: none; + } + + .pagination-new { + flex-direction: column; + gap: 16px; + } + + .page-indicators { + order: -1; + } + + .blog-card-content-new { + padding: 20px; + } +} + +@media (max-width: 480px) { + .homepage-layout .app-title { font-size:1.2rem; } + .blog-card-content-new { padding:16px; } +} + +.blog-section-header { + text-align: center; + margin-bottom: 32px; +} + +.blog-section-title { + font-size: 1.8rem; + font-weight: 700; + color: #1b2735; + margin: 0 0 8px 0; + letter-spacing: -0.5px; +} + +.blog-section-subtitle { + font-size: 1rem; + color: #64748b; + margin: 0; + font-weight: 400; +} + +.blog-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + width: 100%; +} + +.blog-card { + background: linear-gradient(145deg, #ffffff, #f8fafc); + border: 1px solid #e2e8f0; + border-radius: 16px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + position: relative; +} + +.blog-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); + border-color: #cbd5e1; +} + +.blog-card-image { + width: 100%; + height: 160px; + overflow: hidden; + background: linear-gradient(135deg, #f1f5f9, #e2e8f0); +} + +.blog-card-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.blog-card:hover .blog-card-image img { + transform: scale(1.05); +} + +.blog-card-content { + padding: 20px; +} + +.blog-card-tags { + display: flex; + gap: 6px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.blog-card-tag { + background: linear-gradient(135deg, #ddd6fe, #e0e7ff); + color: #6366f1; + font-size: 0.7rem; + font-weight: 600; + padding: 4px 8px; + border-radius: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.blog-card-title { + font-size: 1.1rem; + font-weight: 700; + color: #1e293b; + margin: 0 0 8px 0; + line-height: 1.4; + letter-spacing: -0.3px; +} + +.blog-card-excerpt { + font-size: 0.9rem; + color: #64748b; + line-height: 1.5; + margin: 0 0 16px 0; +} + +.blog-card-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.blog-card-author { + font-size: 0.8rem; + font-weight: 600; + color: #475569; +} + +.blog-card-date { + font-size: 0.75rem; + color: #94a3b8; +} + +.blog-card-stats { + display: flex; + gap: 12px; +} + +.blog-card-stat { + font-size: 0.7rem; + color: #64748b; + font-weight: 500; +} + +/* Blog View Styles */ +.app-root.blog-view { padding-top:0; } +.blog-view .bg-layers { display:none; } +.blog-view .top-bar.improved { position:relative; } + +/* Unified non-sticky blog header (single source of truth) */ +/* .blog-header { position:relative; background:linear-gradient(135deg,#f8fafc 0%, #e2e8f0 100%); border-bottom:0px solid #cbd5e1; padding:40px 0 50px; margin:0; } */ +.blog-header { position:relative; background-color:#f2f5f6; padding:40px 0 10px; margin:0; } +.blog-header.smart-header.collapsed .blog-title { font-size:1.25rem; margin:0; } +.blog-header.smart-header.expanded .blog-title { font-size:1.6rem; margin:0 0 12px; } +.blog-header.smart-header.collapsed .back-button { padding:4px 12px; font-size:0.7rem; margin-bottom:6px; } +.blog-header.smart-header.expanded .back-button { padding:8px 16px; font-size:0.8rem; margin-bottom:16px; } + +.blog-header-inner { + max-width: 800px; + margin: 0 auto; + padding: 0 32px; +} + +.back-button { + background: #6366f1; + border: none; + color: white; + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + padding: 8px 16px; + margin-bottom: 16px; + border-radius: 20px; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.back-button:hover { + background: #4f46e5; + transform: translateY(-1px); +} + +.blog-title-section { + text-align: left; +} + +.blog-title { + font-size: 1.6rem; + font-weight: 700; + color: #1e293b; + margin: 0 0 12px 0; + line-height: 1.3; + letter-spacing: -0.4px; +} + +.blog-meta { + display: flex; + align-items: center; + gap: 20px; + margin-bottom: 12px; + flex-wrap: wrap; +} + +.blog-author, .blog-date { + font-size: 0.85rem; + font-weight: 500; + color: #64748b; + display: flex; + align-items: center; + gap: 4px; +} + +/* (Removed unused header tag styles previously) */ + +.blog-author { + font-size: 0.9rem; + font-weight: 600; + color: #475569; +} + +.blog-date { + font-size: 0.9rem; + color: #64748b; +} + +/* (Obsolete .blog-tags removed) */ + +/* (Removed earlier gradient tag style) */ + +/* (Removed early full-bleed variants) */ + +.featured-image { + margin: 0 0 32px 0; + text-align: center; +} + +.featured-image img { + width: 100%; + max-height: 400px; + object-fit: cover; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); +} + +.featured-image figcaption { + margin-top: 12px; + font-size: 0.9rem; + color: #64748b; + font-style: italic; +} + +/* (Removed earlier temporary blog-body reset; consolidated version appears later) */ + +.blog-image { + margin: 32px 0; + text-align: center; +} + +.blog-image img { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.blog-image figcaption { + margin-top: 8px; + font-size: 0.9rem; + color: #64748b; + font-style: italic; +} + +/* Blog View Layout - Wider and Scrollable */ +.blog-view { overflow-y:auto; height:100vh; background:#f8fafc; } + +.blog-content-area { max-width:900px; margin:0 auto; padding:20px 20px; width:100%; background:#f8fafc; } + +.blog-article { + background: #ffffff; + border: none; + border-radius: 8px; + padding: 5px 5px 5px 5px; + box-shadow: none; + width: 100%; + max-width: none; + margin: 0 auto; +} + +/* Removed duplicate sticky blog-header block - using the earlier static positioned header design */ + +/* (Removed duplicate .blog-header-inner definition) */ + +/* (Removed duplicate .blog-title-section) */ + +/* (Removed earlier oversized .blog-title) */ + +/* (Removed intermediate .blog-meta variant) */ + +/* (Removed duplicate blog-author/blog-date) */ + +/* (Removed second header tags block) */ + +/* Blog Content Styling */ +/* (Removed duplicate featured-image block) */ + +.featured-image img { + width: 100%; + height: auto; + display: block; +} + +.featured-image figcaption { + padding: 1rem 0; + background: transparent; + color: #94a3b8; + font-style: italic; + text-align: center; + font-size: 0.875rem; +} + +/* (Removed duplicate blog-body variant) */ + +/* (Removed earlier heading block) */ + +.blog-body h1:first-child, +.blog-body h2:first-child, +.blog-body h3:first-child { + margin-top: 1.5rem; +} + +.blog-body h2 { + font-size: 1.875rem; + border-bottom: none; + padding-bottom: 0; +} + +.blog-body h3 { + font-size: 1.5rem; +} + +.blog-body h4 { + font-size: 1.25rem; +} + +.blog-body p { margin-bottom:1.25rem; text-align:left; color:#1e293b; font-family:'Inter',system-ui,sans-serif; font-weight:400; font-size:1.05rem; line-height:1.7; letter-spacing:0.15px; } + +.blog-body p:last-child { + margin-bottom: 0; + color: #374151 !important; +} + +/* Apply professional fonts */ +/* (Removed standalone font-family override) */ + +/* (Removed duplicate heading font stack) */ + +.blog-body li, +.blog-body blockquote, +.blog-body span { font-family:'Inter',system-ui,sans-serif; } + +.blog-body img { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 2.5rem 0; + box-shadow: none; + border: none; +} + +/* Storytelling blockquote */ +.blog-body blockquote { border-left:4px solid #6366f1; margin:2rem 0; font-style:italic; color:#24303a !important; background:linear-gradient(90deg,rgba(99,102,241,0.06),rgba(99,102,241,0)); padding:1rem 1.5rem 1rem 1.25rem; border-radius:4px; } + +/* Inline code refined */ +.blog-body code { background:rgba(31,41,55,0.08); padding:0.25rem 0.55rem; border-radius:4px; font-family:'SF Mono','Monaco','Inconsolata','Roboto Mono','Courier New',monospace; font-size:0.85em; color:#111827 !important; } + +.blog-body pre { + background: rgba(15, 23, 42, 0.9); + padding: 1.5rem; + border-radius: 8px; + overflow-x: auto; + margin: 2rem 0; + border: 1px solid rgba(71, 85, 105, 0.5); +} + +.blog-body pre code { + background: none; + padding: 0; + color: #000000 !important; + opacity: 1 !important; +} + +.blog-body ul, .blog-body ol { + margin: 1.5rem 0; + padding-left: 2rem; +} + +.blog-body li { + margin-bottom: 0.5rem; + color: #000000 !important; + opacity: 1 !important; +} + +/* Narrative link styling */ +.blog-body a { color:#1d4ed8; text-decoration:none; position:relative; font-weight:500; transition:color .3s ease; } +.blog-body a:after { content:""; position:absolute; left:0; bottom:-3px; height:2px; width:100%; background:linear-gradient(90deg,#1d4ed8,#6366f1); opacity:.65; transform:scaleX(.35); transform-origin:left; transition:transform .35s ease,opacity .35s ease; } +.blog-body a:hover { color:#1e3a8a; } +.blog-body a:hover:after { transform:scaleX(1); opacity:1; } + +/* Blog Meta and Tags Styles */ +.blog-meta { + display: flex; + gap: 1.5rem; + margin: 1rem 0; + font-size: 0.95rem; + color: #6b7280; +} +/* Removed earlier meta duplications and obsolete .blog-tags */ + +/* New tag styling */ +.blog-tag { background:#f1f5f9; color:#334155; padding:6px 14px; border-radius:999px; font-size:0.7rem; font-weight:600; letter-spacing:0.5px; border:1px solid #e2e8f0; transition:background .25s,color .25s,border-color .25s; } +.blog-tag:hover { background:#e2e8f0; } + +/* Minimal tags separator section */ +.blog-tags-section { margin:3rem 0 2.5rem 0; padding:0; border-top:1px solid #e2e8f0; } +.blog-tags-title { font-family:'Inter',system-ui,sans-serif; font-size:0.7rem; font-weight:600; letter-spacing:1.4px; color:#64748b; margin:1.75rem 0 1rem 0; text-transform:uppercase; padding:0 24px; text-align:left; } +.blog-tags-container { display:flex; flex-wrap:wrap; gap:8px; padding:0 24px; } + +/* Minimal Pagination Styles */ +.pagination-minimal { + margin-top: 2rem; + padding: 1.5rem 0; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.pagination-track { + position: relative; + height: 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + margin-bottom: 1rem; + overflow: hidden; +} + +.pagination-progress { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(90deg, #6366f1 0%, #8b5cf6 100%); + border-radius: 4px; + transition: width 0.3s ease; +} + +.pagination-progress.page-1 { width: 25%; } +.pagination-progress.page-2 { width: 50%; } +.pagination-progress.page-3 { width: 75%; } +.pagination-progress.page-4 { width: 100%; } + +.pagination-handle { + position: absolute; + top: -6px; + width: 20px; + height: 20px; + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + border: 2px solid white; + border-radius: 50%; + cursor: pointer; + transition: left 0.3s ease, transform 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.pagination-handle.page-1 { left: 20%; } +.pagination-handle.page-2 { left: 45%; } +.pagination-handle.page-3 { left: 70%; } +.pagination-handle.page-4 { left: 95%; } + +.pagination-handle:hover { + transform: scale(1.2); +} + +.pagination-handle.dragging { + transform: scale(1.3); + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.5); +} + +.page-indicator { + font-size: 0.7rem; + font-weight: bold; + color: white; +} + +.pagination-controls { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.pagination-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #e2e8f0; + padding: 0.5rem 1rem; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9rem; + font-weight: 500; +} + +.pagination-btn:hover:not(:disabled) { + background: rgba(99, 102, 241, 0.2); + border-color: rgba(99, 102, 241, 0.5); + transform: translateY(-1px); +} + +.pagination-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.pagination-meta { + text-align: center; + color: #9ca3af; + font-size: 0.85rem; +} + +/* Remove old pagination slider styles */ +.pagination-slider, +.slider-container, +.slider-nav, +.slider-track, +.slider-dates, +.date-indicator, +.blog-sidebar, +/* .blog-tags-section removed so it can display */ +.tags-title, +.blog-content-layout { + display: none; +} +/* Removed legacy hidden pagination/slider/sidebar selectors */ + +/* Enhanced Blog Card Meta */ +.blog-card-meta { + display: flex; + gap: 1rem; + margin: 1rem 0 0.5rem 0; + font-size: 0.85rem; + color: #9ca3af; + align-items: center; +} + +.blog-card-author, .blog-card-date { + display: flex; + align-items: center; + gap: 0.25rem; + font-weight: 500; +} + +/* Responsive Design */ +@media (max-width:1100px){ .blog-section{width:86%;} .blog-content-area{padding:20px 40px; max-width:900px;} .blog-header-inner{padding:0 40px;} } + +@media (max-width: 900px) { + .blog-section { width: 90%; } + .blog-grid { gap: 20px; } + .blog-content-area { padding:20px 30px; max-width:900px; } + .blog-header-inner { padding:0 30px; } +} + +@media (max-width: 640px) { + .blog-section { width: 94%; } + .blog-grid { + grid-template-columns: 1fr; + gap: 16px; + } + .blog-card-content { padding: 16px; } + .blog-content-area { padding:20px 20px; max-width:900px; } + .blog-header-inner { padding:0 20px; } + + .pagination-controls { + flex-direction: column; + gap: 0.5rem; + } + + .pagination-btn { + width: 100%; + } +} + +/* Removed sticky override block (header already non-sticky) */ + +/* Unified blog typography & layout (storytelling enhanced) */ +.blog-content-area { max-width:900px; margin:0 auto; padding:0 20px 40px; width:100%; background:#f8fafc; } +.blog-article { background:#ffffff; border:none; border-radius:12px; padding:36px 48px 36px; box-shadow:0 4px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06); width:100%; margin:12px auto 0; } +.blog-body { font-size:1.06rem; line-height:1.72; color:#1f2933; max-width:65ch; margin:0 auto; font-family:'Lora','Merriweather',Georgia,serif; font-weight:400; font-variant-ligatures:common-ligatures; font-kerning:normal; hyphens:auto; } +.blog-body.story-mode { font-size:1.12rem; max-width:60ch; } +.blog-body h1, .blog-body h2, .blog-body h3, .blog-body h4, .blog-body h5, .blog-body h6 { font-family:'Lora','Merriweather',Georgia,serif; font-weight:600; line-height:1.25; letter-spacing:-0.3px; margin:2.2rem 0 1.15rem; color:#14202b; } +.blog-body h1:first-child, .blog-body h2:first-child, .blog-body h3:first-child { margin-top:1rem; } +.blog-body p { margin:0 0 1.3rem; font-weight:400; font-size:inherit; letter-spacing:0.15px; color:#1f2933; } +.blog-body p:last-child { margin-bottom:0; } +.blog-paragraph { margin:0 0 1.3rem; } + diff --git a/frontend/src/style.css.new b/frontend/src/style.css.new new file mode 100644 index 0000000000000000000000000000000000000000..cd75bc7f3d6a195af50fa9fec3088ef459caaad7 --- /dev/null +++ b/frontend/src/style.css.new @@ -0,0 +1,53 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap'); + +* { box-sizing: border-box; } +html, body, #root { height: 100%; } +body { margin:0; font-family:'Inter',system-ui,sans-serif; color:#1b2126; background:#f2f4f8; overflow-x:hidden; -webkit-font-smoothing:antialiased; } + +/* Loading animations */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.75); + display: flex; + justify-content: center; + align-items: center; + border-radius: 18px; + z-index: 10; + backdrop-filter: blur(3px); +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(99, 102, 241, 0.3); + border-radius: 50%; + border-top-color: #6366f1; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.blog-card-new.loading { + pointer-events: none; + opacity: 0.7; +} + +.app-root { position:relative; min-height:100%; padding-top:150px; } + +/* Layered subtle geometric background */ +.bg-layers { position:fixed; inset:0; pointer-events:none; z-index:0; background: + linear-gradient(145deg, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0) 65%), + radial-gradient(circle at 78% 24%, rgba(96,165,250,0.18), transparent 60%), + radial-gradient(circle at 15% 70%, rgba(167,139,250,0.15), transparent 62%), + radial-gradient(circle at 50% 85%, rgba(125,168,255,0.12), transparent 58%), + repeating-linear-gradient(115deg, rgba(0,0,0,0.025) 0 14px, rgba(0,0,0,0) 14px 28px), + linear-gradient(180deg,#f3f5f9,#eef1f6); + mask: linear-gradient(#fff,rgba(255,255,255,0.35)); } + +/* Rest of your existing CSS... */ \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..2bb71d4becf3d3d2036ec3ea35211d59792864c2 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..6abbd86291fd6d2afd521a1fc1e11e4b1f41218f --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + base: '/', + plugins: [react()], + build: { + outDir: 'dist', + sourcemap: true + }, + server: { + port: 5173 + } +}) diff --git a/test.txt b/test.txt new file mode 100644 index 0000000000000000000000000000000000000000..26a4959ac261b876c302ca1b646b3ce35e96ca5d --- /dev/null +++ b/test.txt @@ -0,0 +1,5 @@ +Weather Information for London: +City: London +Country: UK +Temperature: 15Β°C +Conditions: Cloudy \ No newline at end of file