From 764560da45482b7403901fe6ab73409d1deb5f5a Mon Sep 17 00:00:00 2001 From: "Elina K." <145558996+mitanuriel@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:06:59 +0100 Subject: [PATCH 1/3] Add social media manager dashboard with modern UI - Created responsive single-page dashboard for social media post generation - Integrated with existing FastAPI backend - Features: API key storage, multiple model selection, copy to clipboard - Modern dark theme with animations and smooth UX - Added run_server.py for easy startup - Includes comprehensive dashboard documentation --- DASHBOARD.md | 67 +++++++ run_server.py | 12 ++ src/social_media_backend.py | 15 ++ src/static/app.js | 159 +++++++++++++++ src/static/index.html | 116 +++++++++++ src/static/styles.css | 375 ++++++++++++++++++++++++++++++++++++ 6 files changed, 744 insertions(+) create mode 100644 DASHBOARD.md create mode 100644 run_server.py create mode 100644 src/static/app.js create mode 100644 src/static/index.html create mode 100644 src/static/styles.css 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..a9b044f --- /dev/null +++ b/run_server.py @@ -0,0 +1,12 @@ +"""Start the AgentOps Guardian social media manager server.""" + +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "src.social_media_backend:app", + host="0.0.0.0", + port=8000, + reload=True, + log_level="info" + ) diff --git a/src/social_media_backend.py b/src/social_media_backend.py index a8c8578..0d9fcaa 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.""" diff --git a/src/static/app.js b/src/static/app.js new file mode 100644 index 0000000..3d5842a --- /dev/null +++ b/src/static/app.js @@ -0,0 +1,159 @@ +// 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'; + +// 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 @@ + + + + + + AgentOps Guardian - Social Media Manager + + + +
+
+ +

AI-Powered Social Media Manager

+
+ +
+
+

Generate Social Media Post

+

Create engaging content powered by Mistral AI

+ +
+
+ + + Your key is stored locally and never sent to our servers +
+ +
+ + + Be specific about tone, platform, and key points +
+ +
+ + +
+ + +
+
+ + + + + + +
+ + +
+ + + + diff --git a/src/static/styles.css b/src/static/styles.css new file mode 100644 index 0000000..0981317 --- /dev/null +++ b/src/static/styles.css @@ -0,0 +1,375 @@ +:root { + --primary: #6366f1; + --primary-dark: #4f46e5; + --secondary: #8b5cf6; + --success: #10b981; + --error: #ef4444; + --warning: #f59e0b; + --bg-dark: #0f172a; + --bg-card: #1e293b; + --bg-hover: #334155; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --border: #334155; + --shadow: rgba(0, 0, 0, 0.3); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + background: linear-gradient(135deg, var(--bg-dark) 0%, #1e1b4b 100%); + color: var(--text-primary); + min-height: 100vh; + padding: 20px; + line-height: 1.6; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +/* Header */ +header { + text-align: center; + margin-bottom: 40px; + padding: 30px 0; +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + margin-bottom: 10px; +} + +.shield { + font-size: 48px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + color: var(--text-secondary); + font-size: 1.1rem; + margin-top: 8px; +} + +/* Cards */ +.dashboard-card { + background: var(--bg-card); + border-radius: 16px; + padding: 30px; + margin-bottom: 24px; + box-shadow: 0 10px 30px var(--shadow); + border: 1px solid var(--border); + transition: transform 0.2s, box-shadow 0.2s; +} + +.dashboard-card:hover { + transform: translateY(-2px); + box-shadow: 0 15px 40px var(--shadow); +} + +.dashboard-card h2 { + font-size: 1.8rem; + margin-bottom: 8px; + color: var(--text-primary); +} + +.description { + color: var(--text-secondary); + margin-bottom: 24px; +} + +/* Form */ +.form-group { + margin-bottom: 24px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: var(--text-primary); +} + +.label-text { + font-size: 0.95rem; +} + +.required { + color: var(--error); + margin-left: 4px; +} + +input, textarea, select { + width: 100%; + padding: 12px 16px; + background: var(--bg-dark); + border: 2px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 1rem; + transition: border-color 0.3s, box-shadow 0.3s; + font-family: inherit; +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +textarea { + resize: vertical; + min-height: 100px; +} + +.hint { + display: block; + margin-top: 6px; + color: var(--text-secondary); + font-size: 0.85rem; +} + +/* Buttons */ +.btn-primary, .btn-secondary { + width: 100%; + padding: 14px 24px; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + color: white; + box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: var(--bg-hover); + color: var(--text-primary); + border: 2px solid var(--border); +} + +.btn-secondary:hover { + background: var(--border); + border-color: var(--primary); +} + +/* Result Card */ +.result-card { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 12px; +} + +.result-header h3 { + font-size: 1.5rem; + color: var(--text-primary); +} + +.result-meta { + display: flex; + gap: 8px; +} + +.badge { + padding: 6px 12px; + background: var(--bg-dark); + border-radius: 20px; + font-size: 0.85rem; + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.provider-badge { + background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + color: white; + border: none; +} + +.post-output { + background: var(--bg-dark); + padding: 20px; + border-radius: 8px; + border-left: 4px solid var(--primary); + white-space: pre-wrap; + line-height: 1.8; + font-size: 1rem; + margin-bottom: 20px; +} + +.result-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +/* Error Card */ +.error-card { + border-left: 4px solid var(--error); + animation: shake 0.5s; +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-10px); } + 75% { transform: translateX(10px); } +} + +.error-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.error-icon { + font-size: 1.5rem; +} + +.error-card h3 { + color: var(--error); +} + +#errorMessage { + color: var(--text-secondary); + margin-bottom: 16px; +} + +/* Loading */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.95); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 20px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-overlay p { + color: var(--text-secondary); + font-size: 1.1rem; +} + +/* Utility */ +.hidden { + display: none !important; +} + +/* Footer */ +footer { + text-align: center; + padding: 30px 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +footer a { + color: var(--primary); + text-decoration: none; + transition: color 0.3s; +} + +footer a:hover { + color: var(--secondary); +} + +/* Responsive */ +@media (max-width: 640px) { + h1 { + font-size: 2rem; + } + + .shield { + font-size: 36px; + } + + .dashboard-card { + padding: 20px; + } + + .result-actions { + grid-template-columns: 1fr; + } +} From bcc6cbd2e17b0f99ce6eeae15633bbcccfc1229a Mon Sep 17 00:00:00 2001 From: "Elina K." <145558996+mitanuriel@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:35:00 +0100 Subject: [PATCH 2/3] Add lilac theme with wizard wand cursor and enhanced UX - Transformed UI with soft lilac/purple color palette and gold accents - Implemented magical wizard wand cursor with instant response (no delay) - Added Playfair Display font for elegant, high-contrast typography - Enhanced custom cursor with sparkle trail effects - Improved API error handling with detailed user-friendly messages - Added debug logging for API key validation - Created test_api_key.py utility for troubleshooting Mistral API - Optimized cursor follower speed (45% interpolation for smooth tracking) - Added glowing effects and animations throughout the dashboard --- src/social_media_backend.py | 87 +++++++-- src/static/app.js | 54 +++++ src/static/styles.css | 379 +++++++++++++++--------------------- test_api_key.py | 70 +++++++ 4 files changed, 351 insertions(+), 239 deletions(-) create mode 100644 test_api_key.py diff --git a/src/social_media_backend.py b/src/social_media_backend.py index 0d9fcaa..f206ccd 100644 --- a/src/social_media_backend.py +++ b/src/social_media_backend.py @@ -41,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 index 3d5842a..f9e738d 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -17,6 +17,60 @@ 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); diff --git a/src/static/styles.css b/src/static/styles.css index 0981317..425d954 100644 --- a/src/static/styles.css +++ b/src/static/styles.css @@ -1,17 +1,23 @@ +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600&display=swap'); + :root { - --primary: #6366f1; - --primary-dark: #4f46e5; - --secondary: #8b5cf6; + --primary: #c084fc; + --primary-dark: #a855f7; + --secondary: #e9d5ff; + --accent: #fbbf24; --success: #10b981; - --error: #ef4444; - --warning: #f59e0b; - --bg-dark: #0f172a; - --bg-card: #1e293b; - --bg-hover: #334155; - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --border: #334155; - --shadow: rgba(0, 0, 0, 0.3); + --error: #f87171; + --warning: #fbbf24; + --bg-dark: #1e1b2e; + --bg-card: #2d2640; + --bg-hover: #3d3352; + --text-primary: #f5f3ff; + --text-secondary: #d4c5f9; + --border: #4c3a6d; + --shadow: rgba(88, 28, 135, 0.3); + --lilac-light: #e9d5ff; + --lilac-mid: #c084fc; + --lilac-dark: #9333ea; } * { @@ -21,59 +27,110 @@ } body { - font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; - background: linear-gradient(135deg, var(--bg-dark) 0%, #1e1b4b 100%); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + background: linear-gradient(135deg, #1e1b2e 0%, #2d1b3d 50%, #1e1b2e 100%); color: var(--text-primary); min-height: 100vh; padding: 20px; line-height: 1.6; + cursor: none; } -.container { - max-width: 800px; - margin: 0 auto; +.custom-cursor { + width: 40px; + height: 40px; + position: fixed; + pointer-events: none; + z-index: 9999; + transform: translate(-20px, -20px) rotate(-45deg); + filter: drop-shadow(0 0 8px var(--lilac-mid)) drop-shadow(0 0 15px var(--accent)); +} + +.custom-cursor::before { + content: '✨'; + position: absolute; + font-size: 28px; + top: -8px; + left: 2px; + animation: wand-glow 2s ease-in-out infinite; +} + +.custom-cursor::after { + content: ''; + position: absolute; + width: 3px; + height: 35px; + background: linear-gradient(180deg, var(--lilac-dark) 0%, var(--lilac-mid) 100%); + bottom: -5px; + left: 50%; + transform: translateX(-50%); + border-radius: 2px; + box-shadow: 0 0 10px var(--lilac-mid); +} + +@keyframes wand-glow { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.8; transform: scale(1.1); } +} + +.cursor-follower { + width: 12px; + height: 12px; + background: radial-gradient(circle, var(--accent) 0%, var(--lilac-mid) 100%); + border-radius: 50%; + position: fixed; + pointer-events: none; + z-index: 9998; + transform: translate(-50%, -50%); + box-shadow: 0 0 20px var(--lilac-mid), 0 0 30px var(--accent); + animation: twinkle 1.5s ease-in-out infinite; } -/* Header */ -header { - text-align: center; - margin-bottom: 40px; - padding: 30px 0; +@keyframes twinkle { + 0%, 100% { opacity: 0.8; } + 50% { opacity: 1; } } -.logo { - display: flex; - align-items: center; - justify-content: center; - gap: 15px; - margin-bottom: 10px; +.sparkle { + position: fixed; + pointer-events: none; + z-index: 9997; + animation: sparkle-fade 0.6s ease-out forwards; } -.shield { - font-size: 48px; - animation: pulse 2s infinite; +@keyframes sparkle-fade { + 0% { opacity: 1; transform: scale(1) rotate(0deg); } + 100% { opacity: 0; transform: scale(0) rotate(180deg); } } +.container { max-width: 800px; margin: 0 auto; } + +header { text-align: center; margin-bottom: 40px; padding: 30px 0; } + +.logo { display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: 10px; } + +.shield { font-size: 48px; animation: pulse 2s infinite; } + @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } h1 { + font-family: 'Playfair Display', serif; font-size: 2.5rem; - background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + font-weight: 900; + background: linear-gradient(135deg, var(--lilac-light) 0%, var(--lilac-mid) 50%, var(--accent) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + letter-spacing: -0.5px; } -.subtitle { - color: var(--text-secondary); - font-size: 1.1rem; - margin-top: 8px; -} +h2, h3 { font-family: 'Playfair Display', serif; font-weight: 700; } + +.subtitle { color: var(--text-secondary); font-size: 1.1rem; margin-top: 8px; } -/* Cards */ .dashboard-card { background: var(--bg-card); border-radius: 16px; @@ -84,42 +141,19 @@ h1 { transition: transform 0.2s, box-shadow 0.2s; } -.dashboard-card:hover { - transform: translateY(-2px); - box-shadow: 0 15px 40px var(--shadow); -} +.dashboard-card:hover { transform: translateY(-2px); box-shadow: 0 15px 40px var(--shadow); } -.dashboard-card h2 { - font-size: 1.8rem; - margin-bottom: 8px; - color: var(--text-primary); -} +.dashboard-card h2 { font-size: 1.8rem; margin-bottom: 8px; color: var(--text-primary); } -.description { - color: var(--text-secondary); - margin-bottom: 24px; -} +.description { color: var(--text-secondary); margin-bottom: 24px; } -/* Form */ -.form-group { - margin-bottom: 24px; -} +.form-group { margin-bottom: 24px; } -label { - display: block; - margin-bottom: 8px; - font-weight: 600; - color: var(--text-primary); -} +label { display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-primary); } -.label-text { - font-size: 0.95rem; -} +.label-text { font-size: 0.95rem; } -.required { - color: var(--error); - margin-left: 4px; -} +.required { color: var(--error); margin-left: 4px; } input, textarea, select { width: 100%; @@ -135,23 +169,14 @@ input, textarea, select { input:focus, textarea:focus, select:focus { outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); + border-color: var(--lilac-mid); + box-shadow: 0 0 0 3px rgba(192, 132, 252, 0.2); } -textarea { - resize: vertical; - min-height: 100px; -} +textarea { resize: vertical; min-height: 100px; } -.hint { - display: block; - margin-top: 6px; - color: var(--text-secondary); - font-size: 0.85rem; -} +.hint { display: block; margin-top: 6px; color: var(--text-secondary); font-size: 0.85rem; } -/* Buttons */ .btn-primary, .btn-secondary { width: 100%; padding: 14px 24px; @@ -159,7 +184,7 @@ textarea { border-radius: 8px; font-size: 1rem; font-weight: 600; - cursor: pointer; + cursor: none; transition: all 0.3s; display: flex; align-items: center; @@ -168,109 +193,70 @@ textarea { } .btn-primary { - background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + background: linear-gradient(135deg, var(--lilac-mid) 0%, var(--lilac-dark) 100%); color: white; - box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); + box-shadow: 0 4px 15px rgba(192, 132, 252, 0.4); + position: relative; + overflow: hidden; +} + +.btn-primary::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(233, 213, 255, 0.3); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; } -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(99, 102, 241, 0.6); -} +.btn-primary:hover::before { width: 300px; height: 300px; } -.btn-primary:active { - transform: translateY(0); -} +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(192, 132, 252, 0.6); } -.btn-primary:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none; -} +.btn-primary:active { transform: translateY(0); } -.btn-secondary { - background: var(--bg-hover); - color: var(--text-primary); - border: 2px solid var(--border); -} +.btn-primary:disabled { opacity: 0.6; cursor: none; transform: none; } -.btn-secondary:hover { - background: var(--border); - border-color: var(--primary); -} +.btn-secondary { background: var(--bg-hover); color: var(--text-primary); border: 2px solid var(--border); } -/* Result Card */ -.result-card { - animation: slideIn 0.3s ease-out; -} +.btn-secondary:hover { background: var(--border); border-color: var(--lilac-mid); } + +.result-card { animation: slideIn 0.3s ease-out; } @keyframes slideIn { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } } -.result-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - flex-wrap: wrap; - gap: 12px; -} +.result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; } -.result-header h3 { - font-size: 1.5rem; - color: var(--text-primary); -} +.result-header h3 { font-size: 1.5rem; color: var(--text-primary); } -.result-meta { - display: flex; - gap: 8px; -} +.result-meta { display: flex; gap: 8px; } -.badge { - padding: 6px 12px; - background: var(--bg-dark); - border-radius: 20px; - font-size: 0.85rem; - color: var(--text-secondary); - border: 1px solid var(--border); -} +.badge { padding: 6px 12px; background: var(--bg-dark); border-radius: 20px; font-size: 0.85rem; color: var(--text-secondary); border: 1px solid var(--border); } -.provider-badge { - background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); - color: white; - border: none; -} +.provider-badge { background: linear-gradient(135deg, var(--lilac-mid) 0%, var(--accent) 100%); color: var(--bg-dark); border: none; font-weight: 600; } .post-output { background: var(--bg-dark); padding: 20px; border-radius: 8px; - border-left: 4px solid var(--primary); + border-left: 4px solid var(--lilac-mid); white-space: pre-wrap; line-height: 1.8; font-size: 1rem; margin-bottom: 20px; + box-shadow: inset 0 0 20px rgba(192, 132, 252, 0.1); } -.result-actions { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} +.result-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } -/* Error Card */ -.error-card { - border-left: 4px solid var(--error); - animation: shake 0.5s; -} +.error-card { border-left: 4px solid var(--error); animation: shake 0.5s; } @keyframes shake { 0%, 100% { transform: translateX(0); } @@ -278,34 +264,21 @@ textarea { 75% { transform: translateX(10px); } } -.error-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} +.error-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } -.error-icon { - font-size: 1.5rem; -} +.error-icon { font-size: 1.5rem; } -.error-card h3 { - color: var(--error); -} +.error-card h3 { color: var(--error); } -#errorMessage { - color: var(--text-secondary); - margin-bottom: 16px; -} +#errorMessage { color: var(--text-secondary); margin-bottom: 16px; } -/* Loading */ .loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(15, 23, 42, 0.95); + background: rgba(30, 27, 46, 0.95); display: flex; flex-direction: column; align-items: center; @@ -317,59 +290,29 @@ textarea { width: 50px; height: 50px; border: 4px solid var(--border); - border-top-color: var(--primary); + border-top-color: var(--lilac-mid); + border-right-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; + box-shadow: 0 0 30px var(--lilac-mid); } -@keyframes spin { - to { transform: rotate(360deg); } -} +@keyframes spin { to { transform: rotate(360deg); } } -.loading-overlay p { - color: var(--text-secondary); - font-size: 1.1rem; -} +.loading-overlay p { color: var(--text-secondary); font-size: 1.1rem; } -/* Utility */ -.hidden { - display: none !important; -} +.hidden { display: none !important; } -/* Footer */ -footer { - text-align: center; - padding: 30px 0; - color: var(--text-secondary); - font-size: 0.9rem; -} +footer { text-align: center; padding: 30px 0; color: var(--text-secondary); font-size: 0.9rem; } -footer a { - color: var(--primary); - text-decoration: none; - transition: color 0.3s; -} +footer a { color: var(--lilac-mid); text-decoration: none; transition: color 0.3s; } -footer a:hover { - color: var(--secondary); -} +footer a:hover { color: var(--accent); } -/* Responsive */ @media (max-width: 640px) { - h1 { - font-size: 2rem; - } - - .shield { - font-size: 36px; - } - - .dashboard-card { - padding: 20px; - } - - .result-actions { - grid-template-columns: 1fr; - } + h1 { font-size: 2rem; } + .shield { font-size: 36px; } + .dashboard-card { padding: 20px; } + .result-actions { grid-template-columns: 1fr; } } diff --git a/test_api_key.py b/test_api_key.py new file mode 100644 index 0000000..e7fe780 --- /dev/null +++ b/test_api_key.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Quick test script to validate your Mistral API key.""" + +import sys +import requests + +def test_mistral_key(api_key: str) -> None: + """Test if a Mistral API key is valid.""" + # Clean the key + api_key = api_key.strip() + + print(f"Testing API key...") + print(f" Length: {len(api_key)} characters") + print(f" Preview: {api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else " Key too short!") + print() + + # Test with a simple request + url = "https://api.mistral.ai/v1/chat/completions" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + data = { + "model": "mistral-small-latest", + "messages": [{"role": "user", "content": "Say 'hello'"}], + "max_tokens": 10 + } + + try: + print("Sending test request to Mistral API...") + response = requests.post(url, headers=headers, json=data, timeout=30) + + print(f"Response status: {response.status_code}") + + if response.status_code == 200: + print("āœ… SUCCESS! Your API key is valid and working!") + result = response.json() + print(f"Response: {result['choices'][0]['message']['content']}") + elif response.status_code == 401: + print("āŒ AUTHENTICATION FAILED") + print(f"Error: {response.text}") + print("\nPossible issues:") + print(" 1. API key is incorrect or expired") + print(" 2. API key not activated yet (wait a few minutes after redeeming)") + print(" 3. Copy-paste error (check for extra spaces or missing characters)") + print("\nVerify your key at: https://console.mistral.ai/") + else: + print(f"āš ļø Unexpected response: {response.status_code}") + print(f"Details: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"āŒ Network error: {e}") + print("Check your internet connection") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python test_api_key.py YOUR_API_KEY") + print("\nOr set MISTRAL_API_KEY environment variable:") + print(" export MISTRAL_API_KEY='your-key-here'") + print(" python test_api_key.py") + + import os + api_key = os.getenv("MISTRAL_API_KEY") + if api_key: + print("\nFound MISTRAL_API_KEY in environment, testing...") + test_mistral_key(api_key) + else: + sys.exit(1) + else: + test_mistral_key(sys.argv[1]) From cae9ee8f3735df9d770987ab0e3a43faa0b69797 Mon Sep 17 00:00:00 2001 From: "Elina K." <145558996+mitanuriel@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:54:34 +0100 Subject: [PATCH 3/3] Improve security: safer server defaults and secure API key handling - Change run_server.py to bind to 127.0.0.1 by default (not 0.0.0.0) - Disable auto-reload by default in production - Add environment variables (BIND_HOST, BIND_PORT, BIND_RELOAD) for configuration - Remove API key exposure from test_api_key.py (no length/preview logging) - Replace command-line argument with secure getpass prompt - Read API key from MISTRAL_API_KEY environment variable or secure prompt - Prevent API keys from appearing in shell history or terminal output --- run_server.py | 12 +++++++++--- test_api_key.py | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/run_server.py b/run_server.py index a9b044f..70aa2fc 100644 --- a/run_server.py +++ b/run_server.py @@ -1,12 +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="0.0.0.0", - port=8000, - reload=True, + host=host, + port=port, + reload=reload, log_level="info" ) diff --git a/test_api_key.py b/test_api_key.py index e7fe780..d5026ed 100644 --- a/test_api_key.py +++ b/test_api_key.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """Quick test script to validate your Mistral API key.""" +import os import sys +import getpass import requests def test_mistral_key(api_key: str) -> None: @@ -9,9 +11,12 @@ def test_mistral_key(api_key: str) -> None: # Clean the key api_key = api_key.strip() - print(f"Testing API key...") - print(f" Length: {len(api_key)} characters") - print(f" Preview: {api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else " Key too short!") + # Validate without exposing key content + if not api_key: + print("āŒ ERROR: API key is empty") + sys.exit(1) + + print("Testing API key...") print() # Test with a simple request @@ -53,18 +58,23 @@ def test_mistral_key(api_key: str) -> None: print("Check your internet connection") if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: python test_api_key.py YOUR_API_KEY") - print("\nOr set MISTRAL_API_KEY environment variable:") + # Try to get API key from secure sources + api_key = os.getenv("MISTRAL_API_KEY") + + if not api_key: + # Prompt securely without echoing to terminal + try: + api_key = getpass.getpass("Enter your Mistral API key: ") + except (KeyboardInterrupt, EOFError): + print("\nāŒ API key input cancelled") + sys.exit(1) + + if not api_key or not api_key.strip(): + print("āŒ ERROR: No API key provided") + print("\nSet MISTRAL_API_KEY environment variable:") print(" export MISTRAL_API_KEY='your-key-here'") print(" python test_api_key.py") - - import os - api_key = os.getenv("MISTRAL_API_KEY") - if api_key: - print("\nFound MISTRAL_API_KEY in environment, testing...") - test_mistral_key(api_key) - else: - sys.exit(1) - else: - test_mistral_key(sys.argv[1]) + print("\nOr run the script and enter key when prompted (secure input)") + sys.exit(1) + + test_mistral_key(api_key)