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
+
+
+
+
+
+
+
+
+
Generate Social Media Post
+
Create engaging content powered by Mistral AI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Generating your post with Mistral AI...
+
+
+
+
+
+
+
+
+
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)