diff --git a/backend/ai_core/prompts.py b/backend/ai_core/prompts.py index 5670097..c117805 100644 --- a/backend/ai_core/prompts.py +++ b/backend/ai_core/prompts.py @@ -1,8 +1,45 @@ from datetime import datetime +TONE_MAP = { + "beginner": """ +Write in a beginner-friendly style. +Explain concepts using simple language. +Avoid unnecessary jargon. +Use examples wherever possible. +""", + + "professional": """ +Write in a professional technical blogging style. +Maintain clarity and industry-standard explanations. +""", + + "academic": """ +Write in an academic and analytical style. +Provide detailed reasoning and technical depth. +""", + + "humorous": """ +Write in a light-hearted and engaging style. +Use appropriate humor while keeping technical accuracy. +""", + + "concise": """ +Write concise explanations. +Avoid unnecessary details. +Focus on key insights only. +""" +} + def build_prompt(problem, current_time: str) -> str: custom_instructions = "" + tone_instructions = "" + + if hasattr(problem, "tone") and problem.tone: + tone_instructions = TONE_MAP.get( + problem.tone.lower(), + "" + ) default_prompt = f""" You are a professional technical writer and competitive programmer. @@ -48,6 +85,9 @@ def build_prompt(problem, current_time: str) -> str: return f""" {default_prompt} +Selected Writing Tone: +{tone_instructions} + {custom_instructions} """ diff --git a/backend/main.py b/backend/main.py index 69d36a3..319e057 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,31 +1,28 @@ import base64 -from contextlib import asynccontextmanager -from datetime import datetime, timedelta, timezone import hashlib import hmac import json import logging import os import secrets +from contextlib import asynccontextmanager +from datetime import datetime, timedelta, timezone from typing import Annotated, Any, Optional +import httpx import motor.motor_asyncio import uvicorn -import httpx - from dotenv import load_dotenv from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request, status from fastapi.concurrency import run_in_threadpool from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles -from fastapi.responses import RedirectResponse -from dotenv import load_dotenv from pydantic import BaseModel +from pymongo.errors import PyMongoError from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from slowapi.util import get_remote_address -from pymongo.errors import PyMongoError from twilio.rest import Client from ai import rate_code_efficiency @@ -33,11 +30,14 @@ # --- UPDATED AI PATH --- from ai_core.blog_generator import generate_blog, generate_tags from devto import publish_to_platforms +from github_integration import push_solution_to_github from models.reminder import PublishRecord -from services.reminder_scheduler import start_scheduler +from models.user import PlatformCredential from services.complexity_analyzer import analyze_code +from services.credential_service import resolve_user_credentials +from services.reminder_scheduler import start_scheduler from social import share_to_platforms -from github_integration import push_solution_to_github +from utils.crypto import encrypt load_dotenv() @@ -115,6 +115,7 @@ class Problem(BaseModel): language: str | None = None client_time: str | None = None custom_prompt: str | None = None + tone: str | None = None platforms: list[str] | None = None publish_as_draft: bool = False share_to_social: bool = True @@ -471,6 +472,19 @@ async def create_blog( status_code=400, detail="Custom prompt exceeds maximum length of 1000 characters.", ) + allowed_tones = [ + "beginner", + "professional", + "academic", + "humorous", + "concise" + ] + + if problem.tone and problem.tone.lower() not in allowed_tones: + raise HTTPException( + status_code=400, + detail="Invalid tone selected." + ) if not problem.code or problem.code.strip() == "": return {"status": "error", "message": "Code is empty, cannot generate blog."} @@ -491,14 +505,14 @@ async def create_blog( devto_creds = await resolve_user_credentials(db, user_id, "devto") try: - suggested_tags = await run_in_threadpool( + await run_in_threadpool( generate_tags, problem, blog_content, credentials=user_settings, ) except Exception: - suggested_tags = "" + pass try: platform_results = await publish_to_platforms( @@ -679,8 +693,7 @@ async def get_dashboard_stats( user_email = current_user["email"] else: user_email = require_user(x_user_email) - - user_filter = {"user_email": user_email} + user_filter = {"user_email": user_email} try: total = await db.problem_info.count_documents(user_filter) @@ -726,14 +739,12 @@ async def get_dashboard_stats( if daily_activity: dates_set = {doc["date"] for doc in daily_activity} today = datetime.now(timezone.utc).date() - current_date = today if current_date.isoformat() not in dates_set: current_date = today - timedelta(days=1) - - while current_date.isoformat() in dates_set: - current_streak += 1 - current_date -= timedelta(days=1) + while current_date.isoformat() in dates_set: + current_streak += 1 + current_date -= timedelta(days=1) recent_cursor = ( db.problem_info.find( diff --git a/extension/background.js b/extension/background.js index 55f323c..23cf7ab 100644 --- a/extension/background.js +++ b/extension/background.js @@ -6,14 +6,20 @@ function getUserEmail() { }); } -function getUserEmail() { - return new Promise(resolve => { - chrome.storage.local.get({ userEmail: null }, ({ userEmail }) => resolve(userEmail)); - }); -} + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'GENERATE_BLOG') { - const { title, description, code, author, client_time, custom_prompt, difficulty, topics } = request.payload; + const { + title, + description, + code, + author, + client_time, + custom_prompt, + tone, + difficulty, + topics + } = request.payload; chrome.storage.local.get({ publishingPlatforms: ['devto'], publishAsDraft: false, @@ -27,7 +33,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { method: "POST", headers: { "Content-Type": "application/json", "X-User-Email": userEmail }, body: JSON.stringify({ - title, description, code, author, client_time, custom_prompt, difficulty, + title, + description, + code, + author, + client_time, + custom_prompt, + tone, + difficulty, tags: (topics && topics.length > 0) ? topics : null, platforms: publishingPlatforms, publish_as_draft: publishAsDraft diff --git a/extension/content.js b/extension/content.js index eddc3f6..cda6db6 100644 --- a/extension/content.js +++ b/extension/content.js @@ -14,7 +14,10 @@ const AUTO_TRIGGER_MIN_INTERVAL_MS = 60 * 1000; // 1 minute between auto-triggers for same submission // Function to handle data extraction and blog generation - const triggerBlogGeneration = async (custom_prompt = "") => { + const triggerBlogGeneration = async ( + custom_prompt = "", + tone = "beginner" + ) => { if (isProcessing) return; isProcessing = true; @@ -97,7 +100,18 @@ // Send to background script chrome.runtime.sendMessage({ type: 'GENERATE_BLOG', - payload: { title, description, code, author, client_time, custom_prompt, difficulty, language, topics } + payload: { + title, + description, + code, + author, + client_time, + custom_prompt, + tone, + difficulty, + language, + topics + } }); @@ -147,7 +161,10 @@ // Start of Listener for manual triggers from popup and status updates chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === 'MANUAL_TRIGGER') { - triggerBlogGeneration(request.custom_prompt || ""); //usage of custom prompt + triggerBlogGeneration( + request.custom_prompt || "", + request.tone || "beginner" + ); //usage of custom prompt } else if (request.type === 'STATUS_UPDATE') { if (request.status === 'success' || request.status === 'error') { isProcessing = false; diff --git a/extension/popup.html b/extension/popup.html index 5c9dbed..3df0ae4 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -113,6 +113,29 @@ box-shadow: 0 0 0 3px rgba(247, 160, 26, 0.15); background: rgba(0, 0, 0, 0.4); } + select { + width: 100%; + padding: 12px 14px; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--border-glass); + border-radius: 12px; + color: var(--text); + font-size: 13px; + font-family: 'Inter', sans-serif; + outline: none; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + } + + select:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(247, 160, 26, 0.15); + } + + select option { + background: #1a1d29; + color: white; + } textarea::placeholder { color: #606575; @@ -510,6 +533,16 @@

LeetLog AI

+
+ + +
diff --git a/extension/popup.js b/extension/popup.js index cdc0555..cc6661e 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -199,14 +199,20 @@ document.addEventListener('DOMContentLoaded', async () => { chrome.storage.local.get({ publishingPlatforms: ['devto'], - publishAsDraft: false - }, ({ publishingPlatforms, publishAsDraft }) => { + publishAsDraft: false, + selectedTone: 'beginner' + }, ({ publishingPlatforms, publishAsDraft, selectedTone }) => { platformInputs.forEach(input => { input.checked = publishingPlatforms.includes(input.value); }); draftInput.checked = publishAsDraft; + const toneSelect = document.getElementById('toneSelect'); + + if (toneSelect) { + toneSelect.value = selectedTone; + } }); const savePublishingSettings = () => { @@ -227,12 +233,16 @@ document.addEventListener('DOMContentLoaded', async () => { chrome.storage.local.set({ publishingPlatforms: selectedPlatforms, - publishAsDraft: draftInput.checked + publishAsDraft: draftInput.checked, + selectedTone: document.getElementById('toneSelect').value }); }; platformInputs.forEach(input => input.addEventListener('change', savePublishingSettings)); draftInput.addEventListener('change', savePublishingSettings); + document + .getElementById('toneSelect') + ?.addEventListener('change', savePublishingSettings); // Load generated blog from storage chrome.storage.local.get( @@ -317,6 +327,9 @@ document.addEventListener('DOMContentLoaded', async () => { .getElementById('customPrompt') .value .trim(); + const tone = document + .getElementById('toneSelect') + .value; if (!tab || !tab.url || !tab.url.includes("leetcode.com/problems/")) { statusEl.innerText = "Please open a LeetCode problem page!"; @@ -341,7 +354,8 @@ document.addEventListener('DOMContentLoaded', async () => { await chrome.tabs.sendMessage(tab.id, { type: 'MANUAL_TRIGGER', - custom_prompt: customPrompt + custom_prompt: customPrompt, + tone: tone }); } catch (msgErr) { @@ -358,11 +372,19 @@ document.addEventListener('DOMContentLoaded', async () => { if (generateBtn) generateBtn.disabled = false; if (copyBtn) copyBtn.disabled = false; - await chrome.tabs.sendMessage(tab.id, { type: 'MANUAL_TRIGGER' }); + await chrome.tabs.sendMessage(tab.id, { + type: 'MANUAL_TRIGGER', + custom_prompt: customPrompt, + tone: tone + }); setTimeout(async () => { try { - await chrome.tabs.sendMessage(tab.id, { type: 'MANUAL_TRIGGER' }); + await chrome.tabs.sendMessage(tab.id, { + type: 'MANUAL_TRIGGER', + custom_prompt: customPrompt, + tone: tone + }); } catch (e2) { statusEl.innerText = "Error: Please refresh LeetCode page!"; statusEl.className = "error-status";