diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..56d81aa --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,145 @@ +name: Build and Push Docker Images + +on: + push: + branches: [ main, develop, staging, latest ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + OWNER: jockdarock + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + matrix: + include: + - name: frontend + context: ./agent-frontend + dockerfile: ./agent-frontend/Dockerfile.agent.frontend + image: ghcr.io/jockdarock/scribby-agent-frontend + - name: backend + context: ./backend + dockerfile: ./backend/Dockerfile.backend + image: ghcr.io/jockdarock/scribby-backend + - name: backend-agent + context: ./backend-agent + dockerfile: ./backend-agent/Dockerfile.agent + image: ghcr.io/jockdarock/scribby-backend-agent + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ matrix.image }} + tags: | + type=ref,event=branch,suffix=-{{sha}} + type=ref,event=pr,suffix=-{{sha}} + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=Scribby ${{ matrix.name }} + org.opencontainers.image.description=AI-powered transcription and content generation - ${{ matrix.name }} service + org.opencontainers.image.url=https://github.com/jockdarock/scribby + org.opencontainers.image.source=https://github.com/jockdarock/scribby + org.opencontainers.image.licenses=MIT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate image summary + run: | + echo "## Docker Image Built: ${{ matrix.name }}" >> $GITHUB_STEP_SUMMARY + echo "**Image:** \`${{ matrix.image }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + publish-manifest: + runs-on: ubuntu-latest + needs: build-and-push + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Make images public + run: | + # Set all images to public visibility + gh api --method PATCH /user/packages/container/scribby-agent-frontend/versions \ + --field visibility=public || echo "Frontend package not found or already public" + + gh api --method PATCH /user/packages/container/scribby-backend/versions \ + --field visibility=public || echo "Backend package not found or already public" + + gh api --method PATCH /user/packages/container/scribby-backend-agent/versions \ + --field visibility=public || echo "Backend-agent package not found or already public" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate deployment summary + run: | + echo "## 🚀 Scribby Deployment Ready" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "All container images have been built and pushed to GitHub Container Registry:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Container Images" >> $GITHUB_STEP_SUMMARY + echo "| Service | Image | Pull Command |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|--------------|" >> $GITHUB_STEP_SUMMARY + echo "| Frontend | \`ghcr.io/jockdarock/scribby-agent-frontend\` | \`docker pull ghcr.io/jockdarock/scribby-agent-frontend:${{ github.ref_name }}-${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Backend | \`ghcr.io/jockdarock/scribby-backend\` | \`docker pull ghcr.io/jockdarock/scribby-backend:${{ github.ref_name }}-${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Backend Agent | \`ghcr.io/jockdarock/scribby-backend-agent\` | \`docker pull ghcr.io/jockdarock/scribby-backend-agent:${{ github.ref_name }}-${{ github.sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Quick Deploy" >> $GITHUB_STEP_SUMMARY + echo "To deploy these images, update your \`docker-compose.yml\` to use the published images:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`yaml" >> $GITHUB_STEP_SUMMARY + echo "services:" >> $GITHUB_STEP_SUMMARY + echo " transcription-api:" >> $GITHUB_STEP_SUMMARY + echo " image: ghcr.io/jockdarock/scribby-backend:${{ github.ref_name }}-${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo " agent-api:" >> $GITHUB_STEP_SUMMARY + echo " image: ghcr.io/jockdarock/scribby-backend-agent:${{ github.ref_name }}-${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo " frontend:" >> $GITHUB_STEP_SUMMARY + echo " image: ghcr.io/jockdarock/scribby-agent-frontend:${{ github.ref_name }}-${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index d36df23..77e4ff1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ agent-frontend/node_modules +agent-frontend/build backend/.venv backend/__pycache__ backend/outputs diff --git a/README.md b/README.md index 07734c2..6493fd5 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,239 @@ -# Whisper Transcription Application - -This application allows you to transcribe audio files, YouTube videos, and microphone recordings using the Whisper API. The application has been split into two components: - -1. **Backend**: A RESTful API server that handles all transcription functionality -2. **Frontend**: A Gradio web interface that communicates with the backend API - -## Project Structure - -``` -scribby/ -├── backend/ -│ ├── api.py # FastAPI server implementation -│ ├── transcriber.py # Transcription functionality -│ ├── requirements.txt # Backend dependencies -│ └── Dockerfile # Backend container definition -├── frontend/ -│ ├── frontend.py # Gradio interface -│ ├── requirements.txt # Frontend dependencies -│ └── Dockerfile # Frontend container definition -├── outputs/ # Shared directory for transcription outputs -└── docker-compose.yml # Configuration for running both services -``` - -## Features - -- Transcribe audio files (MP3, MP4, WAV, etc.) -- Transcribe YouTube videos by URL -- Transcribe microphone recordings -- Language detection or selection -- Translation to English option -- Large file handling with automatic chunking -- Background processing for long-running tasks - -## Installation and Setup - -### Option 1: Using Docker Compose (Recommended) - -1. Make sure you have Docker and Docker Compose installed -2. Clone this repository -3. Create the directory structure and place the files in their respective locations -4. Run the application: - -```bash -docker-compose up -d -``` - -The frontend will be available at http://localhost:7860, and the backend API at http://localhost:8000. - -### Option 2: Manual Setup - -#### Backend Setup - -1. Create a virtual environment and activate it: - -```bash -python -m venv backend-env -source backend-env/bin/activate # On Windows: backend-env\Scripts\activate -``` - -2. Install dependencies: - -```bash -cd backend -pip install -r requirements.txt -``` - -3. Install system dependencies: - - ffmpeg: For audio processing - - yt-dlp: For YouTube video downloading - -4. Run the backend: - -```bash -uvicorn api:app --host 0.0.0.0 --port 8000 --reload -``` - -#### Frontend Setup - -1. Create a virtual environment and activate it: - -```bash -python -m venv frontend-env -source frontend-env/bin/activate # On Windows: frontend-env\Scripts\activate -``` - -2. Install dependencies: - -```bash -cd frontend -pip install -r requirements.txt -``` - -3. Set the API_BASE_URL environment variable to point to your backend: - -```bash -# Linux/Mac -export API_BASE_URL=http://localhost:8000 - -# Windows -set API_BASE_URL=http://localhost:8000 -``` - -4. Run the frontend: - -```bash -python frontend.py -``` - -The Gradio interface will be available at http://localhost:7860. - -## Using the API - -The backend provides the following endpoints: - -- `GET /models`: Get available transcription models -- `GET /languages`: Get available languages -- `POST /transcribe/file`: Transcribe an audio file -- `POST /transcribe/youtube`: Transcribe a YouTube video -- `GET /status/{job_id}`: Check the status of a transcription job -- `GET /download/{job_id}`: Download the transcription result -- `GET /youtube-info`: Get information about a YouTube video - -For more details, access the Swagger documentation at http://localhost:8000/docs when the backend is running. - -## Environment Variables - -### Backend - -- `BASE_URL`: Base URL for the Whisper API (default: `https://litellm.darock.io/v1`) -- `DEBUG`: Enable detailed logging (default: `true`) - -### Frontend - -- `API_BASE_URL`: URL of the backend API (default: `http://localhost:8000`) - -## License - +# Whisper Transcription Application + +This application allows you to transcribe audio files, YouTube videos, and microphone recordings using the Whisper API. The application has been split into two components: + +1. **Backend**: A RESTful API server that handles all transcription functionality +2. **Frontend**: A React web interface that communicates with the backend API +3. **Agent**: An AI-powered content generation service for creating social media posts and blog content + +## Project Structure + +``` +scribby/ +├── backend/ # Transcription API (FastAPI) +│ ├── api.py # FastAPI server implementation +│ ├── transcriber.py # Transcription functionality +│ ├── requirements.txt # Backend dependencies +│ └── Dockerfile.backend # Backend container definition +├── backend-agent/ # Content generation API (FastAPI) +│ ├── agent.py # Agent API implementation +│ ├── requirements.txt # Agent dependencies +│ └── Dockerfile.agent # Agent container definition +├── agent-frontend/ # React frontend +│ ├── src/ # React source code +│ ├── package.json # Frontend dependencies +│ └── Dockerfile.agent.frontend # Frontend container definition +├── outputs/ # Shared directory for transcription outputs +├── docker-compose.yml # Development deployment +├── docker-compose.production.yml # Production deployment with published images +└── .github/workflows/ # GitHub Actions for CI/CD +``` + +## Features + +### Transcription Features +- Transcribe audio files (MP3, MP4, WAV, etc.) +- Transcribe YouTube videos by URL +- Transcribe microphone recordings +- Language detection or selection +- Translation to English option +- Large file handling with automatic chunking +- Background processing for long-running tasks + +### Content Generation Features +- AI-powered social media post generation +- Blog post creation +- Multi-platform content optimization (LinkedIn, Twitter, Instagram, etc.) +- Content revision and editing +- Document assistance + +## Quick Start + +### Option 1: Use Published Images (Recommended for Production) + +```bash +# Start with pre-built images from GitHub Container Registry +./docker.sh start --prod + +# Or manually +docker-compose -f docker-compose.production.yml up -d +``` + +### Option 2: Build Locally (Development) + +```bash +# Build and start all services +./docker.sh start + +# Or manually +docker-compose up -d --build +``` + +### Option 3: Local Development Setup + +```bash +# Run all services locally without Docker +python startup.py +``` + +## Access the Application + +Once started, the application will be available at: +- **Frontend**: http://localhost:3000 +- **Backend API**: http://localhost:8000 +- **Agent API**: http://localhost:8001 +- **API Documentation**: http://localhost:8000/docs (Backend), http://localhost:8001/docs (Agent) + +## Installation and Setup + +### Using Docker (Recommended) + +1. **Prerequisites:** + - Docker and Docker Compose installed + - Git + +2. **Clone and run:** + ```bash + git clone + cd scribby + ./docker.sh start + ``` + +### Manual Setup + +For detailed manual setup instructions, see the individual service directories: +- [Backend Setup](./backend/) +- [Agent Setup](./backend-agent/) +- [Frontend Setup](./agent-frontend/) + +## Configuration + +### API Keys +Configure your API keys through the frontend settings page: +- **Transcription API Key**: For Whisper/OpenAI transcription +- **LLM API Key**: For content generation (OpenAI/custom LLM) + +### Environment Variables + +#### Backend +- `BASE_URL`: Whisper API endpoint (default: `https://api.openai.com/v1`) +- `DEBUG`: Enable detailed logging (default: `true`) + +#### Agent +- `TRANSCRIPTION_API_URL`: Internal transcription service URL +- `LLM_API_URL`: LLM API endpoint (default: `https://api.openai.com/v1`) +- `DEBUG`: Enable debug logging (default: `true`) + +#### Frontend +- `REACT_APP_TRANSCRIPTION_API_URL`: Backend API URL +- `REACT_APP_AGENT_API_URL`: Agent API URL + +## Deployment + +### Production Deployment + +For production deployments, use the published Docker images: + +```bash +# Use production compose file with published images +docker-compose -f docker-compose.production.yml up -d +``` + +See [GitHub Container Registry Guide](./README.ghcr.md) for detailed information about using published images. + +### Custom Deployment + +The application can be deployed on: +- Docker Swarm +- Kubernetes +- Cloud platforms (AWS, GCP, Azure) +- VPS servers + +See [Docker Documentation](./README.docker.md) for deployment details. + +## API Usage + +The backend provides RESTful APIs for transcription and content generation: + +### Transcription API Endpoints +- `GET /models`: Get available transcription models +- `GET /languages`: Get available languages +- `POST /transcribe/file`: Transcribe an audio file +- `POST /transcribe/youtube`: Transcribe a YouTube video +- `GET /status/{job_id}`: Check transcription job status +- `GET /download/{job_id}`: Download transcription result + +### Agent API Endpoints +- `POST /generate`: Generate content from transcription +- `GET /status/{job_id}`: Check generation job status +- `GET /platforms`: Get available social media platforms +- `POST /revise`: Revise existing content +- `POST /document-assist`: AI document assistance + +For complete API documentation, visit the Swagger docs at `/docs` when the services are running. + +## Development + +### GitHub Actions CI/CD + +The project includes automated GitHub Actions workflows that: +- Build Docker images for all services +- Push images to GitHub Container Registry +- Run tests and security scans +- Support multi-platform builds (AMD64, ARM64) + +### Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test with `./docker.sh start` +5. Submit a pull request + +### Local Development Commands + +```bash +# Start development environment +./docker.sh start + +# View logs +./docker.sh logs + +# Rebuild services +./docker.sh build + +# Clean up +./docker.sh clean + +# Use production images +./docker.sh start --prod +``` + +## Troubleshooting + +### Common Issues + +1. **Port conflicts**: Ensure ports 3000, 8000, and 8001 are available +2. **API key errors**: Configure API keys in the frontend settings +3. **Docker issues**: Try `./docker.sh clean` and restart +4. **Build failures**: Check the GitHub Actions logs + +### Debug Mode + +Enable debug logging by setting `DEBUG=true` in environment variables. + +### Getting Help + +- Check the [GitHub Issues](https://github.com/jockdarock/scribby/issues) +- Review service logs: `./docker.sh logs` +- Consult the API documentation at `/docs` + +## Documentation + +- [Docker Setup Guide](./README.docker.md) +- [GitHub Container Registry](./README.ghcr.md) +- [API Documentation](http://localhost:8000/docs) (when running) + +## License + This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/agent-frontend/.dockerignore b/agent-frontend/.dockerignore new file mode 100644 index 0000000..ba0a92c --- /dev/null +++ b/agent-frontend/.dockerignore @@ -0,0 +1,45 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production build +build/ +dist/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Environment variables +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Misc +.eslintcache diff --git a/agent-frontend/Dockerfile.agent.frontend b/agent-frontend/Dockerfile.agent.frontend index a09eb4e..0ad309e 100644 --- a/agent-frontend/Dockerfile.agent.frontend +++ b/agent-frontend/Dockerfile.agent.frontend @@ -2,7 +2,7 @@ FROM node:20-alpine as build WORKDIR /app -# Install dependencies +# Install dependencies (including dev dependencies needed for build) COPY package.json package-lock.json ./ RUN npm ci @@ -15,14 +15,36 @@ RUN npm run build # Production stage FROM nginx:stable-alpine +# Install curl for health checks +RUN apk add --no-cache curl + # Copy build files from build stage COPY --from=build /app/build /usr/share/nginx/html -# Copy custom nginx config if needed -# COPY nginx.conf /etc/nginx/conf.d/default.conf +# Create a basic nginx config for React Router (SPA) +RUN echo 'server {' > /etc/nginx/conf.d/default.conf && \ + echo ' listen 80;' >> /etc/nginx/conf.d/default.conf && \ + echo ' server_name localhost;' >> /etc/nginx/conf.d/default.conf && \ + echo ' root /usr/share/nginx/html;' >> /etc/nginx/conf.d/default.conf && \ + echo ' index index.html;' >> /etc/nginx/conf.d/default.conf && \ + echo ' ' >> /etc/nginx/conf.d/default.conf && \ + echo ' # Handle React Router (SPA)' >> /etc/nginx/conf.d/default.conf && \ + echo ' location / {' >> /etc/nginx/conf.d/default.conf && \ + echo ' try_files $uri $uri/ /index.html;' >> /etc/nginx/conf.d/default.conf && \ + echo ' }' >> /etc/nginx/conf.d/default.conf && \ + echo ' ' >> /etc/nginx/conf.d/default.conf && \ + echo ' # Security headers' >> /etc/nginx/conf.d/default.conf && \ + echo ' add_header X-Frame-Options "SAMEORIGIN" always;' >> /etc/nginx/conf.d/default.conf && \ + echo ' add_header X-XSS-Protection "1; mode=block" always;' >> /etc/nginx/conf.d/default.conf && \ + echo ' add_header X-Content-Type-Options "nosniff" always;' >> /etc/nginx/conf.d/default.conf && \ + echo '}' >> /etc/nginx/conf.d/default.conf # Expose port EXPOSE 80 +# Add health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 + # Start nginx CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/agent-frontend/package-lock.json b/agent-frontend/package-lock.json index 8e7cb04..112d77d 100644 --- a/agent-frontend/package-lock.json +++ b/agent-frontend/package-lock.json @@ -12,6 +12,8 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "docx": "^9.5.1", + "jspdf": "^3.0.1", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", @@ -4315,11 +4317,12 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", + "version": "24.0.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", + "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -4356,6 +4359,13 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -5311,6 +5321,18 @@ "node": ">= 4.0.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -5648,6 +5670,16 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -5817,6 +5849,18 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5951,6 +5995,26 @@ } ] }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -6502,6 +6566,16 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -7184,6 +7258,41 @@ "node": ">=6.0.0" } }, + "node_modules/docx": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz", + "integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.0.1", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -7255,6 +7364,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -8474,6 +8593,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -9241,6 +9366,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -9516,6 +9651,20 @@ } } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -9674,6 +9823,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -12443,6 +12598,24 @@ "node": ">=0.10.0" } }, + "node_modules/jspdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", + "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.7", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -12457,6 +12630,54 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -12534,6 +12755,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -14110,6 +14340,12 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -16715,6 +16951,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -17197,6 +17443,12 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -17459,6 +17711,16 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -17990,6 +18252,16 @@ "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -18373,6 +18645,16 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -18707,9 +18989,10 @@ "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -18958,6 +19241,16 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -19819,6 +20112,24 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", diff --git a/agent-frontend/package.json b/agent-frontend/package.json index 33b1e58..af52e61 100644 --- a/agent-frontend/package.json +++ b/agent-frontend/package.json @@ -7,6 +7,8 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "docx": "^9.5.1", + "jspdf": "^3.0.1", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", diff --git a/agent-frontend/src/App.jsx b/agent-frontend/src/App.jsx index 233eddf..2f02728 100644 --- a/agent-frontend/src/App.jsx +++ b/agent-frontend/src/App.jsx @@ -15,6 +15,7 @@ import ProcessingStatus from './components/ProcessingStatus'; import ResultsDisplay from './components/ResultsDisplay'; import Settings from './components/Settings'; import DocumentEditor from './components/DocumentEditor'; +import SocialMediaCreator from './components/SocialMediaCreator'; // Main App component function AppContent() { @@ -156,6 +157,7 @@ function AppContent() { /> } /> } /> + } /> diff --git a/agent-frontend/src/components/DocumentEditor.jsx b/agent-frontend/src/components/DocumentEditor.jsx index 4785a50..f4c3713 100644 --- a/agent-frontend/src/components/DocumentEditor.jsx +++ b/agent-frontend/src/components/DocumentEditor.jsx @@ -2,6 +2,8 @@ import React, { useState, useContext, useRef, useEffect } from 'react'; import { SettingsContext } from './SettingsContext'; import toast from 'react-hot-toast'; import './DocumentPreview.css'; +import { Document, Packer, Paragraph, TextRun, HeadingLevel, LevelFormat, AlignmentType } from 'docx'; +import jsPDF from 'jspdf'; const DocumentEditor = () => { const { settings } = useContext(SettingsContext); @@ -114,6 +116,23 @@ const DocumentEditor = () => { return `${codeBlockPlaceholder}${codeBlocks.length - 1}`; }); + // Helper function to process inline formatting + const processInlineFormatting = (text) => { + return text + // Handle inline code (but not code blocks) + .replace(/`([^`]+)`/g, '$1') + + // Handle bold and italic + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + + // Handle links + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + + // Handle images + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1'); + }; + // Handle lists - this is more complex to get right const lines = htmlContent.split('\n'); let htmlLines = []; @@ -135,7 +154,8 @@ const DocumentEditor = () => { inOrderedList = false; } const text = line.replace(/^- (.*)/, '$1'); - htmlLines.push(`
  • ${text}
  • `); + const processedText = processInlineFormatting(text); + htmlLines.push(`
  • ${processedText}
  • `); } else if (isOrderedItem) { if (!inOrderedList) { htmlLines.push('
      '); @@ -146,7 +166,8 @@ const DocumentEditor = () => { inUnorderedList = false; } const text = line.replace(/^\d+\. (.*)/, '$1'); - htmlLines.push(`
    1. ${text}
    2. `); + const processedText = processInlineFormatting(text); + htmlLines.push(`
    3. ${processedText}
    4. `); } else { if (inUnorderedList) { htmlLines.push(''); @@ -167,22 +188,14 @@ const DocumentEditor = () => { .replace(/^##### (.*$)/, '
      $1
      ') .replace(/^###### (.*$)/, '
      $1
      ') - // Handle inline code (but not code blocks) - .replace(/`([^`]+)`/g, '$1') - - // Handle bold and italic - .replace(/\*\*([^*]+)\*\*/g, '$1') - .replace(/\*([^*]+)\*/g, '$1') - - // Handle links - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - - // Handle images - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') - // Handle blockquotes .replace(/^> (.*$)/, '
      $1
      '); + // Apply inline formatting to non-heading, non-blockquote lines + if (!processedLine.match(/^/) && !processedLine.match(/^
      /)) { + processedLine = processInlineFormatting(processedLine); + } + // Add paragraphs for non-empty lines that aren't already wrapped in HTML tags if (processedLine.trim() && !processedLine.match(/^<[^>]+>/) && !processedLine.includes(codeBlockPlaceholder)) { processedLine = `

      ${processedLine}

      `; @@ -371,6 +384,409 @@ const DocumentEditor = () => { .replace(/\n/g, '
      '); }; + const handleWordExport = async () => { + try { + toast.loading('Generating Word document...', { id: 'word-export' }); + + const content = markdownContent || ''; + if (!content.trim()) { + toast.error('Document is empty. Please add some content before exporting.'); + return; + } + + // Parse markdown content into document structure + const lines = content.split('\n'); + const docElements = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (!line) { + // Add empty paragraph for spacing + docElements.push(new Paragraph({ text: '' })); + continue; + } + + // Handle headings + if (line.startsWith('#')) { + const level = line.match(/^#+/)[0].length; + const text = line.replace(/^#+\s*/, ''); + const headingLevel = Math.min(level, 6); + + docElements.push( + new Paragraph({ + text: text, + heading: [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3, + HeadingLevel.HEADING_4, HeadingLevel.HEADING_5, HeadingLevel.HEADING_6][headingLevel - 1], + spacing: { before: 240, after: 120 } + }) + ); + } + // Handle blockquotes + else if (line.startsWith('>')) { + const quoteText = line.replace(/^>\s*/, ''); + docElements.push( + new Paragraph({ + children: [new TextRun({ text: quoteText, italics: true })], + indent: { left: 720 }, + spacing: { before: 120, after: 120 } + }) + ); + } + // Handle unordered lists + else if (line.startsWith('- ')) { + const text = line.replace(/^-\s*/, ''); + docElements.push( + new Paragraph({ + text: text, + bullet: { level: 0 }, + spacing: { before: 60, after: 60 } + }) + ); + } + // Handle ordered lists + else if (line.match(/^\d+\.\s/)) { + const text = line.replace(/^\d+\.\s*/, ''); + docElements.push( + new Paragraph({ + text: text, + numbering: { reference: 'my-numbering', level: 0 }, + spacing: { before: 60, after: 60 } + }) + ); + } + // Handle code blocks + else if (line.startsWith('```')) { + // Find the end of the code block + let codeContent = ''; + let j = i + 1; + while (j < lines.length && !lines[j].trim().startsWith('```')) { + codeContent += lines[j] + '\n'; + j++; + } + i = j; // Skip to the end of the code block + + docElements.push( + new Paragraph({ + children: [new TextRun({ + text: codeContent.trim(), + font: { name: 'Courier New' }, + size: 20 + })], + spacing: { before: 120, after: 120 }, + indent: { left: 360 } + }) + ); + } + // Handle regular paragraphs + else { + // Process inline formatting properly + const children = []; + let remaining = line; + + // Parse the text to handle inline formatting + while (remaining) { + // Look for bold text + const boldMatch = remaining.match(/\*\*([^*]+)\*\*/); + if (boldMatch) { + // Add text before bold + if (boldMatch.index > 0) { + children.push(new TextRun({ text: remaining.substring(0, boldMatch.index) })); + } + // Add bold text + children.push(new TextRun({ text: boldMatch[1], bold: true })); + remaining = remaining.substring(boldMatch.index + boldMatch[0].length); + continue; + } + + // Look for italic text + const italicMatch = remaining.match(/\*([^*]+)\*/); + if (italicMatch) { + // Add text before italic + if (italicMatch.index > 0) { + children.push(new TextRun({ text: remaining.substring(0, italicMatch.index) })); + } + // Add italic text + children.push(new TextRun({ text: italicMatch[1], italics: true })); + remaining = remaining.substring(italicMatch.index + italicMatch[0].length); + continue; + } + + // Look for inline code + const codeMatch = remaining.match(/`([^`]+)`/); + if (codeMatch) { + // Add text before code + if (codeMatch.index > 0) { + children.push(new TextRun({ text: remaining.substring(0, codeMatch.index) })); + } + // Add code text + children.push(new TextRun({ + text: codeMatch[1], + font: { name: 'Courier New' }, + size: 20 + })); + remaining = remaining.substring(codeMatch.index + codeMatch[0].length); + continue; + } + + // No more formatting, add remaining text + children.push(new TextRun({ text: remaining })); + break; + } + + // If no content was processed, add the original line + if (children.length === 0) { + children.push(new TextRun({ text: line })); + } + + docElements.push( + new Paragraph({ + children: children, + spacing: { before: 120, after: 120 } + }) + ); + } + } + + // Create the document with proper structure and compatibility + const doc = new Document({ + creator: 'Scribby Document Editor', + title: 'Scribby Document', + description: 'Document created with Scribby', + numbering: { + config: [ + { + reference: 'my-numbering', + levels: [ + { + level: 0, + format: LevelFormat.DECIMAL, + text: '%1.', + alignment: AlignmentType.LEFT, + style: { + paragraph: { + indent: { left: 720, hanging: 360 }, + }, + }, + }, + ], + }, + ], + }, + sections: [ + { + properties: {}, + children: docElements.length > 0 ? docElements : [ + new Paragraph({ + text: 'Empty Document', + spacing: { before: 120, after: 120 } + }) + ] + } + ] + }); + + // Generate and download the document + const buffer = await Packer.toBlob(doc); + const url = URL.createObjectURL(buffer); + const link = document.createElement('a'); + link.href = url; + link.download = `scribby-document-${new Date().toISOString().slice(0, 10)}.docx`; + link.style.display = 'none'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + toast.success('Word document exported successfully!', { id: 'word-export' }); + } catch (error) { + console.error('Word export error:', error); + toast.error('Failed to export Word document. Please try again.', { id: 'word-export' }); + } + }; + + const handlePDFExport = async () => { + try { + toast.loading('Generating PDF document...', { id: 'pdf-export' }); + + const content = markdownContent || ''; + if (!content.trim()) { + toast.error('Document is empty. Please add some content before exporting.'); + return; + } + + // Create a new jsPDF instance + const doc = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4' + }); + + // Set up document properties + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const margin = 20; + const maxLineWidth = pageWidth - 2 * margin; + + let yPosition = margin; + const lineHeight = 7; + const paragraphSpacing = 10; + + // Parse markdown content line by line + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Skip empty lines but add some spacing + if (!line) { + yPosition += lineHeight / 2; + continue; + } + + // Check if we need a new page + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + + // Handle headings + if (line.startsWith('#')) { + const level = line.match(/^#+/)[0].length; + const text = line.replace(/^#+\s*/, ''); + + // Set font size based on heading level + const fontSize = Math.max(20 - (level - 1) * 2, 12); + doc.setFontSize(fontSize); + doc.setFont('helvetica', 'bold'); + + yPosition += paragraphSpacing; + + // Split text if it's too long + const textLines = doc.splitTextToSize(text, maxLineWidth); + textLines.forEach((textLine) => { + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + doc.text(textLine, margin, yPosition); + yPosition += lineHeight + 2; + }); + + yPosition += paragraphSpacing / 2; + } + // Handle blockquotes + else if (line.startsWith('>')) { + const quoteText = line.replace(/^>\s*/, ''); + doc.setFontSize(11); + doc.setFont('helvetica', 'italic'); + + yPosition += paragraphSpacing / 2; + + const textLines = doc.splitTextToSize(quoteText, maxLineWidth - 20); + textLines.forEach((textLine) => { + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + doc.text(textLine, margin + 10, yPosition); + yPosition += lineHeight; + }); + + yPosition += paragraphSpacing / 2; + } + // Handle lists + else if (line.startsWith('- ') || line.match(/^\d+\.\s/)) { + const text = line.replace(/^[-\d.]\s*/, ''); + const bullet = line.startsWith('- ') ? '• ' : '1. '; + + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + + const textLines = doc.splitTextToSize(text, maxLineWidth - 20); + textLines.forEach((textLine, index) => { + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + const prefix = index === 0 ? bullet : ' '; + doc.text(prefix + textLine, margin + 10, yPosition); + yPosition += lineHeight; + }); + } + // Handle code blocks + else if (line.startsWith('```')) { + // Find the end of the code block + let codeContent = ''; + let j = i + 1; + while (j < lines.length && !lines[j].trim().startsWith('```')) { + codeContent += lines[j] + '\n'; + j++; + } + i = j; // Skip to the end of the code block + + doc.setFontSize(10); + doc.setFont('courier', 'normal'); + + yPosition += paragraphSpacing / 2; + + const codeLines = codeContent.split('\n'); + codeLines.forEach((codeLine) => { + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + doc.text(codeLine, margin + 5, yPosition); + yPosition += lineHeight; + }); + + yPosition += paragraphSpacing / 2; + } + // Handle regular paragraphs + else { + // Process inline formatting (simplified) + let processedText = line + .replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold markers for PDF + .replace(/\*([^*]+)\*/g, '$1') // Remove italic markers for PDF + .replace(/`([^`]+)`/g, '$1'); // Remove inline code markers for PDF + + doc.setFontSize(12); + doc.setFont('helvetica', 'normal'); + + const textLines = doc.splitTextToSize(processedText, maxLineWidth); + textLines.forEach((textLine) => { + if (yPosition > pageHeight - margin) { + doc.addPage(); + yPosition = margin; + } + doc.text(textLine, margin, yPosition); + yPosition += lineHeight; + }); + + yPosition += paragraphSpacing / 2; + } + } + + // Add page numbers + const pageCount = doc.internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text(`Page ${i} of ${pageCount}`, pageWidth - margin - 20, pageHeight - 10); + } + + // Save the PDF + doc.save(`scribby-document-${new Date().toISOString().slice(0, 10)}.pdf`); + + toast.success('PDF document exported successfully!', { id: 'pdf-export' }); + } catch (error) { + console.error('PDF export error:', error); + toast.error('Failed to export PDF document. Please try again.', { id: 'pdf-export' }); + } + }; + const renderEditor = () => { switch (activeTab) { case 'text': // Preview Tab - Shows rendered content @@ -546,6 +962,27 @@ const DocumentEditor = () => { 📝 Markdown (.md) +
      + + )} diff --git a/agent-frontend/src/components/DocumentPreview.css b/agent-frontend/src/components/DocumentPreview.css index 69e5000..fa0e9f9 100644 --- a/agent-frontend/src/components/DocumentPreview.css +++ b/agent-frontend/src/components/DocumentPreview.css @@ -134,6 +134,20 @@ color: #e5e7eb; } +.dark .document-preview ul, +.dark .document-preview ol, +.dark .document-preview li { + color: #e5e7eb; +} + +.dark .document-preview strong { + color: #f9fafb; +} + +.dark .document-preview em { + color: #e5e7eb; +} + .dark .document-preview blockquote { background-color: #374151; color: #d1d5db; diff --git a/agent-frontend/src/components/Sidebar.jsx b/agent-frontend/src/components/Sidebar.jsx index 13b98fd..9602bf6 100644 --- a/agent-frontend/src/components/Sidebar.jsx +++ b/agent-frontend/src/components/Sidebar.jsx @@ -14,6 +14,15 @@ const Sidebar = ({ isOpen, onClose }) => { ) }, + { + path: '/social-creator', + label: 'Social Media Creator', + icon: ( + + + + ) + }, { path: '/document-editor', label: 'Document Editor', diff --git a/agent-frontend/src/components/SocialMediaCreator.jsx b/agent-frontend/src/components/SocialMediaCreator.jsx new file mode 100644 index 0000000..33ea0b9 --- /dev/null +++ b/agent-frontend/src/components/SocialMediaCreator.jsx @@ -0,0 +1,1080 @@ +import React, { useState, useContext, useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; +import toast from 'react-hot-toast'; +import { SettingsContext } from './SettingsContext'; + +// Platform-specific icons +const PlatformIcon = ({ platform }) => { + const iconClasses = "h-5 w-5"; + + switch (platform) { + case 'LinkedIn': + return ( + + + + ); + case 'Twitter': + return ( + + + + ); + case 'BlueSky': + return ( + + + + ); + case 'Instagram': + return ( + + + + ); + case 'Facebook': + return ( + + + + ); + case 'TikTok': + return ( + + + + ); + default: + return ( + + + + ); + } +}; + +// Platform Card Component +const PlatformCard = ({ platform, content, settings }) => { + const [isEditing, setIsEditing] = useState(false); + const [editedText, setEditedText] = useState(content.text); + const [isCopied, setIsCopied] = useState(false); + const [isRevisioning, setIsRevisioning] = useState(false); + const [revisionInstructions, setRevisionInstructions] = useState(''); + const [isRevisionLoading, setIsRevisionLoading] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(isEditing ? editedText : content.text); + setIsCopied(true); + toast.success(`${platform} content copied to clipboard!`); + setTimeout(() => setIsCopied(false), 2000); + }; + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleSave = () => { + setIsEditing(false); + content.text = editedText; + content.character_count = editedText.length; + }; + + const handleCancel = () => { + setIsEditing(false); + setEditedText(content.text); + }; + + const handleRevisionSubmit = async () => { + if (!revisionInstructions.trim()) { + toast.error('Please provide revision instructions'); + return; + } + + if (!settings.llmApiKey) { + toast.error('LLM API key is required for revision. Please check your settings.'); + return; + } + + setIsRevisionLoading(true); + + try { + const agentApiUrl = settings.llmApiUrl || process.env.REACT_APP_AGENT_API_URL || 'http://localhost:8001'; + const response = await fetch(`${agentApiUrl}/revise`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: editedText, + platform: platform, + content_type: 'social_media', + instructions: revisionInstructions, + llm_api_key: settings.llmApiKey, + llm_model: settings.defaultLlmModel, + llm_base_url: settings.llmBaseUrl + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Revision request failed: ${errorText}`); + } + + const data = await response.json(); + setEditedText(data.revised_content); + content.text = data.revised_content; + content.character_count = data.revised_content.length; + + setIsRevisioning(false); + setRevisionInstructions(''); + toast.success('Content revised successfully!'); + + } catch (error) { + console.error('Revision error:', error); + toast.error('Failed to revise content. Please try again.'); + } finally { + setIsRevisionLoading(false); + } + }; + + // Platform-specific styles + const platformStyles = { + LinkedIn: 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900/30 dark:border-blue-800 dark:text-blue-200', + Twitter: 'bg-sky-100 border-sky-300 text-sky-800 dark:bg-sky-900/30 dark:border-sky-800 dark:text-sky-200', + BlueSky: 'bg-cyan-100 border-cyan-300 text-cyan-800 dark:bg-cyan-900/30 dark:border-cyan-800 dark:text-cyan-200', + Instagram: 'bg-pink-100 border-pink-300 text-pink-800 dark:bg-pink-900/30 dark:border-pink-800 dark:text-pink-200', + Facebook: 'bg-indigo-100 border-indigo-300 text-indigo-800 dark:bg-indigo-900/30 dark:border-indigo-800 dark:text-indigo-200', + TikTok: 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200', + }; + + const style = platformStyles[platform] || 'bg-gray-100 border-gray-300 text-gray-800 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200'; + + const charLimit = { + LinkedIn: 3000, + Twitter: 280, + BlueSky: 300, + Instagram: 2200, + Facebook: 63206, + TikTok: 2200, + }[platform] || 1000; + + const charCount = isEditing ? editedText.length : content.character_count; + const isOverLimit = charCount > charLimit; + + return ( +
      +
      +
      + +

      {platform}

      +
      +
      + + + +
      +
      + + {isEditing ? ( +
      +