Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions backend/ai_core/prompts.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -48,6 +85,9 @@ def build_prompt(problem, current_time: str) -> str:
return f"""
{default_prompt}

Selected Writing Tone:
{tone_instructions}

{custom_instructions}
"""

Expand Down
49 changes: 30 additions & 19 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
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

# --- 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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."}
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
27 changes: 20 additions & 7 deletions extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
23 changes: 20 additions & 3 deletions extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
}
});


Expand Down Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -510,6 +533,16 @@ <h2>LeetLog AI</h2>

<div class="input-group">
<label for="customPrompt">Custom Prompt (Optional)</label>
<div class="input-group">
<label for="toneSelect">Writing Tone</label>
<select id="toneSelect">
<option value="beginner">Beginner Friendly</option>
<option value="professional">Professional</option>
<option value="academic">Academic</option>
<option value="humorous">Humorous</option>
<option value="concise">Concise</option>
</select>
</div>
<textarea id="customPrompt" placeholder="e.g. Make it more humorous, explain time complexity in detail..."></textarea>
</div>

Expand Down
34 changes: 28 additions & 6 deletions extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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(
Expand Down Expand Up @@ -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!";
Expand All @@ -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) {
Expand All @@ -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";
Expand Down
Loading