Skip to content

Commit

Permalink
Merge pull request #5 from rasulkireev/pay-gates
Browse files Browse the repository at this point in the history
Pay gates
  • Loading branch information
rasulkireev authored Dec 7, 2024
2 parents 3493e19 + c18e296 commit 6394fe9
Show file tree
Hide file tree
Showing 14 changed files with 379 additions and 189 deletions.
24 changes: 16 additions & 8 deletions core/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ class ProjectScanIn(Schema):
url: str


# TODO: what is the best practice on handling optional fields?
class ProjectScanOut(Schema):
project_id: int
status: str
message: str = ""
project_id: int = 0
name: str = ""
type: str = ""
url: str
url: str = ""
summary: str = ""


Expand All @@ -24,15 +27,18 @@ class TitleSuggestionOut(Schema):


class GenerateTitleSuggestionsOut(Schema):
suggestions: list[TitleSuggestionOut]
suggestions: list[TitleSuggestionOut] = []
status: str
message: str = ""


class GeneratedContentOut(Schema):
status: str
content: str
slug: str
tags: str
description: str
message: str = ""
content: str = ""
slug: str = ""
tags: str = ""
description: str = ""


class GenerateTitleFromIdeaIn(Schema):
Expand All @@ -41,4 +47,6 @@ class GenerateTitleFromIdeaIn(Schema):


class GenerateTitleSuggestionOut(Schema):
suggestion: dict
status: str
message: str = ""
suggestion: dict = {}
47 changes: 40 additions & 7 deletions core/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

logger = get_seo_blog_bot_logger(__name__)

api = NinjaAPI(auth=MultipleAuthSchema(), csrf=True) # Enable CSRF protection
api = NinjaAPI(auth=MultipleAuthSchema(), csrf=True)


@api.post("/scan", response=ProjectScanOut)
Expand All @@ -35,17 +35,21 @@ def scan_project(request: HttpRequest, data: ProjectScanIn):

# Check if project already exists for this user
project = Project.objects.filter(profile=profile, url=data.url).first()

if project:
return {
"status": "success",
"project_id": project.id,
"has_details": bool(project.name),
"has_suggestions": project.blog_post_title_suggestions.exists(),
}

# Create new project
project = Project.objects.create(profile=profile, url=data.url)
if Project.objects.filter(url=data.url).exists():
return {
"status": "error",
"message": "Project already exists",
}

project = Project.objects.create(profile=profile, url=data.url)
try:
# Get page content from Jina
jina_url = f"https://r.jina.ai/{data.url}"
Expand Down Expand Up @@ -106,6 +110,7 @@ def scan_project(request: HttpRequest, data: ProjectScanIn):
raise ValueError(f"Error processing URL: {str(e)}")

return {
"status": "success",
"project_id": project.id,
"name": project.name,
"type": project.get_type_display(),
Expand All @@ -119,6 +124,17 @@ def generate_title_suggestions(request: HttpRequest, data: GenerateTitleSuggesti
profile = request.auth
project = get_object_or_404(Project, id=data.project_id, profile=profile)

if (
profile.reached_title_generation_limit
# don't want to generate 15 new suggestions if user has a bunch already.
or profile.number_of_title_suggestions + 15 >= 20
):
return {
"suggestions": [],
"status": "error",
"message": "Title generation limit reached. Consider <a class='underline' href='/pricing'>upgrading</a>?",
}

try:
prompt = render_to_string("generate_blog_titles.txt", {"project": project})

Expand Down Expand Up @@ -169,7 +185,10 @@ def generate_title_suggestions(request: HttpRequest, data: GenerateTitleSuggesti
if suggestions:
BlogPostTitleSuggestion.objects.bulk_create(suggestions)

return {"suggestions": titles}
return {
"suggestions": titles,
"status": "success",
}

except json.JSONDecodeError as e:
logger.error(
Expand All @@ -187,7 +206,14 @@ def generate_title_suggestions(request: HttpRequest, data: GenerateTitleSuggesti

@api.post("/generate-blog-content/{suggestion_id}", response=GeneratedContentOut)
def generate_blog_content(request: HttpRequest, suggestion_id: int):
suggestion = get_object_or_404(BlogPostTitleSuggestion, id=suggestion_id, project__profile=request.auth)
profile = request.auth
suggestion = get_object_or_404(BlogPostTitleSuggestion, id=suggestion_id, project__profile=profile)

if profile.reached_content_generation_limit:
return {
"status": "error",
"message": "Content generation limit reached. Consider <a class='underline' href='/pricing'>upgrading</a>?",
}

try:
claude = anthropic.Client(api_key=settings.ANTHROPIC_API_KEY)
Expand Down Expand Up @@ -273,6 +299,12 @@ def generate_title_from_idea(request: HttpRequest, data: GenerateTitleFromIdeaIn
profile = request.auth
project = get_object_or_404(Project, id=data.project_id, profile=profile)

if profile.reached_title_generation_limit:
return {
"status": "error",
"message": "Title generation limit reached. Consider <a class='underline' href='/pricing'>upgrading</a>?",
}

try:
prompt = render_to_string(
"generate_blog_title_based_on_user_prompt.txt", {"project": project, "user_prompt": data.user_prompt}
Expand Down Expand Up @@ -302,12 +334,13 @@ def generate_title_from_idea(request: HttpRequest, data: GenerateTitleFromIdeaIn
)

return {
"status": "success",
"suggestion": {
"id": suggestion.id,
"title": suggestion.title,
"description": suggestion.description,
"category": suggestion.get_category_display(),
}
},
}

except Exception as e:
Expand Down
26 changes: 26 additions & 0 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ def current_state(self):
latest_transition = self.state_transitions.latest("created_at")
return latest_transition.to_state

@property
def has_active_subscription(self):
return self.current_state in [ProfileStates.SUBSCRIBED, ProfileStates.CANCELLED] or self.user.is_superuser

@property
def number_of_active_projects(self):
return self.projects.count()

@property
def number_of_generated_blog_posts(self):
projects = self.projects.all()
return sum(project.generated_blog_posts.count() for project in projects)

@property
def number_of_title_suggestions(self):
projects = self.projects.all()
return sum(project.blog_post_title_suggestions.count() for project in projects)

@property
def reached_content_generation_limit(self):
return self.number_of_generated_blog_posts >= 5 and not self.has_active_subscription

@property
def reached_title_generation_limit(self):
return self.number_of_title_suggestions >= 20 and not self.has_active_subscription


class ProfileStates(models.TextChoices):
STRANGER = "stranger"
Expand Down
5 changes: 5 additions & 0 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,8 @@ class ProjectDetailView(LoginRequiredMixin, DetailView):
def get_queryset(self):
# Ensure users can only see their own projects
return Project.objects.filter(profile=self.request.user.profile)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["has_content_access"] = self.request.user.profile.has_active_subscription
return context
21 changes: 21 additions & 0 deletions frontend/src/controllers/collapse_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
static targets = ["content", "chevron"];

connect() {
this.expanded = true;
}

toggle() {
this.expanded = !this.expanded;

if (this.hasContentTarget) {
this.contentTarget.classList.toggle("hidden");
}

if (this.hasChevronTarget) {
this.chevronTarget.classList.toggle("rotate-180");
}
}
}
8 changes: 6 additions & 2 deletions frontend/src/controllers/content_idea_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import { showMessage } from "../utils/messages";

export default class extends Controller {
static targets = ["form", "input"];
Expand Down Expand Up @@ -44,6 +45,10 @@ export default class extends Controller {

const data = await response.json();

if (data.status === "error") {
throw new Error(data.message);
}

// Add the new suggestion to the list
const suggestionsList = document.querySelector("[data-title-suggestions-target='suggestionsList']");
const suggestionHtml = this.createSuggestionHTML(data.suggestion);
Expand All @@ -58,8 +63,7 @@ export default class extends Controller {
.classList.remove("hidden");

} catch (error) {
console.error("Error:", error);
alert("Failed to generate suggestion. Please try again.");
showMessage(error.message || "Failed to generate suggestion. Please try again later.", 'error');
} finally {
// Restore button state
button.disabled = false;
Expand Down
17 changes: 10 additions & 7 deletions frontend/src/controllers/generate-content-controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from "@hotwired/stimulus";

import { showMessage } from "../utils/messages";
export default class extends Controller {
static values = {
url: String,
Expand Down Expand Up @@ -51,6 +51,11 @@ export default class extends Controller {

const data = await response.json();

// Check if the response indicates an error
if (data.status === "error") {
throw new Error(data.message || "Generation failed");
}

// Update status icon to checkmark with dropdown button
this.statusTarget.innerHTML = `
<div class="flex gap-x-2 items-center">
Expand Down Expand Up @@ -91,18 +96,16 @@ export default class extends Controller {
this.contentTarget.appendChild(contentContainer);

} catch (error) {
console.error("Error:", error);
// Show error state and restore button
this.statusTarget.innerHTML = `
<div class="w-5 h-5 rounded-full border-2 border-gray-300"></div>
`;
showMessage(error.message || "Failed to generate content. Please try again later.", 'error');
// reset the button
this.buttonContainerTarget.innerHTML = `
<button
data-action="generate-content#generate"
class="px-3 py-1 text-sm font-semibold text-white bg-pink-600 rounded-md hover:bg-pink-700">
Generate Content
Generate
</button>
`;
this.statusTarget.innerHTML = "";
}
}

Expand Down
14 changes: 2 additions & 12 deletions frontend/src/controllers/project_details_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import { showMessage } from "../utils/messages";

export default class extends Controller {
static values = {
Expand Down Expand Up @@ -38,18 +39,7 @@ export default class extends Controller {
}, 3000);

} catch (error) {
console.error("Error updating project details:", error);

// Show error message
const errorMessage = document.createElement("div");
errorMessage.className = "mt-2 text-sm text-red-600";
errorMessage.textContent = "Failed to update project details";
form.appendChild(errorMessage);

// Remove error message after 3 seconds
setTimeout(() => {
errorMessage.remove();
}, 3000);
showMessage(error.message || "Failed to update project details", 'error');
}
}
}
20 changes: 6 additions & 14 deletions frontend/src/controllers/scan_progress_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus";
import { showMessage } from "../utils/messages";

export default class extends Controller {
static targets = [
Expand Down Expand Up @@ -42,9 +43,11 @@ export default class extends Controller {
}

const scanData = await response.json();
console.log('Scan response:', scanData);

// Update UI for project details
if (scanData.status === 'error') {
throw new Error(scanData.message);
}

this.detailsSpinnerTarget.classList.add('hidden');
this.detailsCheckTarget.classList.remove('hidden');

Expand Down Expand Up @@ -91,18 +94,7 @@ export default class extends Controller {
this.resultsButtonTarget.classList.remove('hidden');

} catch (error) {
console.error('Error:', error);
this.progressTarget.classList.add('hidden');

if (!this.hasErrorTarget) {
const errorDiv = document.createElement('div');
errorDiv.setAttribute('data-scan-progress-target', 'error');
errorDiv.className = 'mt-2 text-sm text-red-600';
this.element.appendChild(errorDiv);
}

this.errorTarget.classList.remove('hidden');
this.errorTarget.textContent = error.message;
showMessage(error.message || "Failed to scan URL", 'error');
}
}

Expand Down
Loading

0 comments on commit 6394fe9

Please sign in to comment.