Generate Social Media Post
+Create engaging content powered by Mistral AI
+ + +diff --git a/DASHBOARD.md b/DASHBOARD.md new file mode 100644 index 0000000..78fb5a3 --- /dev/null +++ b/DASHBOARD.md @@ -0,0 +1,67 @@ +# Social Media Manager Dashboard + +A beautiful, modern dashboard for generating social media posts using Mistral AI. + +## Quick Start + +1. **Install dependencies:** + ```bash + uv pip install -r requirements.txt + ``` + +2. **Get your Mistral API Key:** + - Sign up at [Mistral AI](https://console.mistral.ai/) + - Generate an API key from your dashboard + +3. **Start the server:** + ```bash + python run_server.py + ``` + +4. **Open the dashboard:** + Navigate to `http://localhost:8000` in your browser + +## Features + +- šØ **Modern UI** - Clean, responsive design with dark theme +- ā” **Fast Generation** - Powered by Mistral AI models +- š **Secure** - API key stored locally in browser +- š **Easy Copy** - One-click copy to clipboard +- šÆ **Model Selection** - Choose between Small, Medium, or Large models +- š¾ **Auto-save** - Your API key is remembered across sessions + +## Usage + +1. Enter your Mistral API key (stored securely in your browser) +2. Describe the social media post you want to create +3. Select the AI model (Small is fastest, Large is highest quality) +4. Click "Generate Post" and wait for the magic ⨠+5. Copy the result and use it on your social media platforms! + +## API Endpoints + +- `GET /` - Dashboard UI +- `GET /health` - Health check +- `POST /generate-post` - Generate social media post + +## Keyboard Shortcuts + +- `Ctrl/Cmd + Enter` - Submit form and generate post + +## Development + +The dashboard consists of: +- **Backend**: FastAPI server ([src/social_media_backend.py](src/social_media_backend.py)) +- **Frontend**: HTML/CSS/JavaScript ([src/static/](src/static/)) +- **Launcher**: Quick start script ([run_server.py](run_server.py)) + +## Troubleshooting + +**Port already in use?** +Edit [run_server.py](run_server.py) and change the port number. + +**API key not working?** +Make sure you're using a valid Mistral API key from https://console.mistral.ai/ + +**Can't connect to server?** +Check that the server is running and visit `http://localhost:8000` diff --git a/run_server.py b/run_server.py new file mode 100644 index 0000000..70aa2fc --- /dev/null +++ b/run_server.py @@ -0,0 +1,18 @@ +"""Start the AgentOps Guardian social media manager server.""" + +import os +import uvicorn + +if __name__ == "__main__": + # Read configuration from environment variables with safer defaults + host = os.getenv("BIND_HOST", "127.0.0.1") + port = int(os.getenv("BIND_PORT", "8000")) + reload = os.getenv("BIND_RELOAD", "false").lower() in ("true", "1", "yes") + + uvicorn.run( + "src.social_media_backend:app", + host=host, + port=port, + reload=reload, + log_level="info" + ) diff --git a/src/social_media_backend.py b/src/social_media_backend.py index a8c8578..f206ccd 100644 --- a/src/social_media_backend.py +++ b/src/social_media_backend.py @@ -2,12 +2,21 @@ import requests from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from pydantic import BaseModel +from pathlib import Path MISTRAL_CHAT_COMPLETIONS_URL = "https://api.mistral.ai/v1/chat/completions" app = FastAPI(title="Social Media Manager Backend") +# Get the directory where this file is located +BASE_DIR = Path(__file__).resolve().parent + +# Mount static files +app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static") + class GeneratePostRequest(BaseModel): """Request payload for social media post generation.""" @@ -17,6 +26,12 @@ class GeneratePostRequest(BaseModel): model: str = "mistral-small-latest" +@app.get("/") +def index(): + """Serve the frontend dashboard.""" + return FileResponse(str(BASE_DIR / "static" / "index.html")) + + @app.get("/health") def health() -> dict: """Health endpoint for quick service checks.""" @@ -26,25 +41,70 @@ def health() -> dict: @app.post("/generate-post") def generate_post(payload: GeneratePostRequest) -> dict: """Generate a social media post from a user prompt using Mistral.""" - response = requests.post( - MISTRAL_CHAT_COMPLETIONS_URL, - headers={ - "Authorization": f"Bearer {payload.api_key}", - "Content-Type": "application/json", - }, - json={ + try: + # Clean the API key (remove any whitespace) + clean_api_key = payload.api_key.strip() + + # Log key format for debugging (first/last 4 chars only) + key_preview = f"{clean_api_key[:4]}...{clean_api_key[-4:]}" if len(clean_api_key) > 8 else "***" + print(f"DEBUG: Using API key: {key_preview}, length: {len(clean_api_key)}") + + response = requests.post( + MISTRAL_CHAT_COMPLETIONS_URL, + headers={ + "Authorization": f"Bearer {clean_api_key}", + "Content-Type": "application/json", + }, + json={ + "model": payload.model, + "messages": [{"role": "user", "content": payload.prompt}], + }, + timeout=60, + ) + + # Handle API errors with helpful messages + if response.status_code == 401: + from fastapi import HTTPException + error_msg = response.json().get("message", "") if response.text else "" + print(f"DEBUG: 401 error details - {error_msg}") + raise HTTPException( + status_code=401, + detail=f"Invalid API key. Check: 1) Key is correctly copied from Mistral console 2) No extra spaces 3) Key is activated. API response: {error_msg}" + ) + elif response.status_code == 429: + from fastapi import HTTPException + raise HTTPException( + status_code=429, + detail="Rate limit exceeded. Please wait a moment and try again." + ) + elif response.status_code >= 400: + from fastapi import HTTPException + error_detail = response.json().get("error", {}).get("message", "Unknown error") + raise HTTPException( + status_code=response.status_code, + detail=f"Mistral API error: {error_detail}" + ) + + response.raise_for_status() + + completion = response.json() + post_text = completion["choices"][0]["message"]["content"] + + return { + "post": post_text, "model": payload.model, - "messages": [{"role": "user", "content": payload.prompt}], - }, - timeout=60, - ) - response.raise_for_status() - - completion = response.json() - post_text = completion["choices"][0]["message"]["content"] - - return { - "post": post_text, - "model": payload.model, - "provider": "mistral", - } + "provider": "mistral", + } + + except requests.exceptions.Timeout: + from fastapi import HTTPException + raise HTTPException( + status_code=504, + detail="Request timeout. The API took too long to respond. Please try again." + ) + except requests.exceptions.RequestException as e: + from fastapi import HTTPException + raise HTTPException( + status_code=500, + detail=f"Network error: {str(e)}" + ) diff --git a/src/static/app.js b/src/static/app.js new file mode 100644 index 0000000..f9e738d --- /dev/null +++ b/src/static/app.js @@ -0,0 +1,213 @@ +// DOM Elements +const postForm = document.getElementById('postForm'); +const generateBtn = document.getElementById('generateBtn'); +const resultSection = document.getElementById('resultSection'); +const errorSection = document.getElementById('errorSection'); +const loadingSection = document.getElementById('loadingSection'); +const postOutput = document.getElementById('postOutput'); +const errorMessage = document.getElementById('errorMessage'); +const modelBadge = document.getElementById('modelBadge'); +const copyBtn = document.getElementById('copyBtn'); +const newPostBtn = document.getElementById('newPostBtn'); +const dismissErrorBtn = document.getElementById('dismissErrorBtn'); + +// API Configuration +const API_BASE_URL = window.location.origin; + +// Local Storage for API Key +const STORAGE_KEY = 'mistral_api_key'; + +// Custom Cursor +const cursor = document.createElement('div'); +cursor.className = 'custom-cursor'; +document.body.appendChild(cursor); + +const follower = document.createElement('div'); +follower.className = 'cursor-follower'; +document.body.appendChild(follower); + +let mouseX = 0, mouseY = 0; +let followerX = 0, followerY = 0; + +document.addEventListener('mousemove', (e) => { + mouseX = e.clientX; + mouseY = e.clientY; + + // Instant cursor response - no delay + cursor.style.left = mouseX + 'px'; + cursor.style.top = mouseY + 'px'; + + // Create sparkle effect more frequently + if (Math.random() > 0.93) { + createSparkle(mouseX, mouseY); + } +}); + +// Much faster follower animation +function animateFollower() { + followerX += (mouseX - followerX) * 0.45; + followerY += (mouseY - followerY) * 0.45; + + follower.style.left = followerX + 'px'; + follower.style.top = followerY + 'px'; + + requestAnimationFrame(animateFollower); +} +animateFollower(); + +// Sparkle effect +function createSparkle(x, y) { + const sparkle = document.createElement('div'); + sparkle.className = 'sparkle'; + sparkle.style.left = x + 'px'; + sparkle.style.top = y + 'px'; + sparkle.style.width = '6px'; + sparkle.style.height = '6px'; + sparkle.style.background = 'linear-gradient(135deg, #c084fc, #fbbf24)'; + sparkle.style.borderRadius = '50%'; + sparkle.style.boxShadow = '0 0 10px #c084fc'; + document.body.appendChild(sparkle); + + setTimeout(() => sparkle.remove(), 600); +} + +// Load saved API key on page load +window.addEventListener('DOMContentLoaded', () => { + const savedKey = localStorage.getItem(STORAGE_KEY); + if (savedKey) { + document.getElementById('apiKey').value = savedKey; + } +}); + +// Form submission handler +postForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const apiKey = document.getElementById('apiKey').value.trim(); + const prompt = document.getElementById('prompt').value.trim(); + const model = document.getElementById('model').value; + + if (!apiKey || !prompt) { + showError('Please fill in all required fields'); + return; + } + + // Save API key to local storage + localStorage.setItem(STORAGE_KEY, apiKey); + + // Hide previous results/errors + hideAllSections(); + + // Show loading + loadingSection.classList.remove('hidden'); + generateBtn.disabled = true; + + try { + const response = await fetch(`${API_BASE_URL}/generate-post`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: apiKey, + prompt: prompt, + model: model + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `Server error: ${response.status}`); + } + + const data = await response.json(); + displayResult(data); + + } catch (error) { + console.error('Error generating post:', error); + showError(error.message || 'Failed to generate post. Please check your API key and try again.'); + } finally { + loadingSection.classList.add('hidden'); + generateBtn.disabled = false; + } +}); + +// Display result +function displayResult(data) { + hideAllSections(); + + postOutput.textContent = data.post; + modelBadge.textContent = data.model; + + resultSection.classList.remove('hidden'); + + // Smooth scroll to result + resultSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +} + +// Show error +function showError(message) { + hideAllSections(); + + errorMessage.textContent = message; + errorSection.classList.remove('hidden'); + + // Smooth scroll to error + errorSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +} + +// Hide all sections +function hideAllSections() { + resultSection.classList.add('hidden'); + errorSection.classList.add('hidden'); +} + +// Copy to clipboard +copyBtn.addEventListener('click', async () => { + const text = postOutput.textContent; + + try { + await navigator.clipboard.writeText(text); + + // Visual feedback + const originalText = copyBtn.innerHTML; + copyBtn.innerHTML = 'ā Copied!'; + copyBtn.style.background = 'var(--success)'; + + setTimeout(() => { + copyBtn.innerHTML = originalText; + copyBtn.style.background = ''; + }, 2000); + + } catch (error) { + console.error('Failed to copy:', error); + showError('Failed to copy to clipboard'); + } +}); + +// Generate new post +newPostBtn.addEventListener('click', () => { + hideAllSections(); + document.getElementById('prompt').focus(); + window.scrollTo({ top: 0, behavior: 'smooth' }); +}); + +// Dismiss error +dismissErrorBtn.addEventListener('click', () => { + errorSection.classList.add('hidden'); +}); + +// Keyboard shortcuts +document.addEventListener('keydown', (e) => { + // Ctrl/Cmd + Enter to submit form + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + postForm.dispatchEvent(new Event('submit')); + } +}); + +// Auto-resize textarea +const promptTextarea = document.getElementById('prompt'); +promptTextarea.addEventListener('input', function() { + this.style.height = 'auto'; + this.style.height = (this.scrollHeight) + 'px'; +}); diff --git a/src/static/index.html b/src/static/index.html new file mode 100644 index 0000000..225b7f0 --- /dev/null +++ b/src/static/index.html @@ -0,0 +1,116 @@ + + +
+ + +AI-Powered Social Media Manager
+Create engaging content powered by Mistral AI
+ + +