Compare commits
2 Commits
926ac150fd
...
bbf0aedfdc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf0aedfdc | ||
|
|
e067dc759c |
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated manually to update status field choices
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('writer', '0005_move_content_fields_to_content'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# No database changes needed - just updating Python-level choices
|
||||||
|
# Tasks status: queued, completed
|
||||||
|
# Content status: draft, review, published
|
||||||
|
# Existing data will remain valid
|
||||||
|
]
|
||||||
|
|
||||||
@@ -9,10 +9,6 @@ class Tasks(SiteSectorBaseModel):
|
|||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('queued', 'Queued'),
|
('queued', 'Queued'),
|
||||||
('in_progress', 'In Progress'),
|
|
||||||
('draft', 'Draft'),
|
|
||||||
('review', 'Review'),
|
|
||||||
('published', 'Published'),
|
|
||||||
('completed', 'Completed'),
|
('completed', 'Completed'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -110,7 +106,12 @@ class Content(SiteSectorBaseModel):
|
|||||||
secondary_keywords = models.JSONField(default=list, blank=True, help_text="List of secondary keywords")
|
secondary_keywords = models.JSONField(default=list, blank=True, help_text="List of secondary keywords")
|
||||||
tags = models.JSONField(default=list, blank=True, help_text="List of tags")
|
tags = models.JSONField(default=list, blank=True, help_text="List of tags")
|
||||||
categories = models.JSONField(default=list, blank=True, help_text="List of categories")
|
categories = models.JSONField(default=list, blank=True, help_text="List of categories")
|
||||||
status = models.CharField(max_length=50, default='draft', help_text="Content workflow status (draft, review, published, etc.)")
|
STATUS_CHOICES = [
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('review', 'Review'),
|
||||||
|
('published', 'Published'),
|
||||||
|
]
|
||||||
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, published)")
|
||||||
generated_at = models.DateTimeField(auto_now_add=True)
|
generated_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,47 @@ const HTMLContentRenderer: React.FC<HTMLContentRendererProps> = ({
|
|||||||
|
|
||||||
// If content is a string, try to parse as JSON first
|
// If content is a string, try to parse as JSON first
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
// Try to parse as JSON (content outline from GPT-4o mini)
|
// Check if it's a JSON string that contains the actual content
|
||||||
|
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
// If it's a full AI response JSON with a 'content' field, use that
|
||||||
|
if (parsed.content && typeof parsed.content === 'string') {
|
||||||
|
// Recursively process the extracted content
|
||||||
|
const extractedContent = parsed.content;
|
||||||
|
// Check if extracted content is HTML
|
||||||
|
if (isHTML(extractedContent)) {
|
||||||
|
const sanitized = sanitizeHTML(extractedContent);
|
||||||
|
if (sanitized.trim().startsWith('<article') || sanitized.trim().startsWith('<div')) {
|
||||||
|
return `<div class="normalized-html-content">${sanitized}</div>`;
|
||||||
|
}
|
||||||
|
return `<div class="normalized-html-content"><article>${sanitized}</article></div>`;
|
||||||
|
}
|
||||||
|
// If extracted content is still JSON, try parsing again
|
||||||
|
if (extractedContent.trim().startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const nestedParsed = JSON.parse(extractedContent);
|
||||||
|
if (nestedParsed.H2 || nestedParsed.H3 || nestedParsed.introduction || nestedParsed.sections) {
|
||||||
|
return formatContentOutline(nestedParsed);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not nested JSON, continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use extracted content as-is (will be processed below)
|
||||||
|
content = extractedContent;
|
||||||
|
} else if (parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections) {
|
||||||
|
// It's a content outline structure
|
||||||
|
return formatContentOutline(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, continue with HTML/text processing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as JSON (content outline from GPT-4o mini) - for non-brace-starting JSON
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
if (typeof parsed === 'object' && (parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections)) {
|
if (typeof parsed === 'object' && (parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections)) {
|
||||||
|
|||||||
@@ -169,12 +169,35 @@ const ToggleMetadata: React.FC<ToggleMetadataProps> = ({ row, contentMetadata })
|
|||||||
contentMetadata?.metadata?.categories ||
|
contentMetadata?.metadata?.categories ||
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const metaDescription =
|
// Extract meta_description, avoiding JSON strings
|
||||||
row.meta_description ||
|
let metaDescription: string | null = null;
|
||||||
row.content_meta_description ||
|
|
||||||
contentMetadata?.meta_description ||
|
// Try direct fields first
|
||||||
contentMetadata?.metadata?.meta_description ||
|
if (row.meta_description && typeof row.meta_description === 'string') {
|
||||||
null;
|
metaDescription = row.meta_description;
|
||||||
|
} else if (row.content_meta_description && typeof row.content_meta_description === 'string') {
|
||||||
|
metaDescription = row.content_meta_description;
|
||||||
|
} else if (contentMetadata?.meta_description && typeof contentMetadata.meta_description === 'string') {
|
||||||
|
metaDescription = contentMetadata.meta_description;
|
||||||
|
} else if (contentMetadata?.metadata?.meta_description && typeof contentMetadata.metadata.meta_description === 'string') {
|
||||||
|
metaDescription = contentMetadata.metadata.meta_description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If metaDescription looks like JSON, try to parse it
|
||||||
|
if (metaDescription && metaDescription.trim().startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(metaDescription);
|
||||||
|
// If parsed object has meta_description, use that
|
||||||
|
if (parsed.meta_description && typeof parsed.meta_description === 'string') {
|
||||||
|
metaDescription = parsed.meta_description;
|
||||||
|
} else {
|
||||||
|
// If it's a full JSON response, extract meta_description from it
|
||||||
|
metaDescription = parsed.meta_description || null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, keep as is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasMetadata =
|
const hasMetadata =
|
||||||
primaryKeyword ||
|
primaryKeyword ||
|
||||||
@@ -263,18 +286,6 @@ const ToggleMetadata: React.FC<ToggleMetadataProps> = ({ row, contentMetadata })
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
<div className="html-content-wrapper">
|
|
||||||
<HTMLContentRenderer
|
|
||||||
content={content}
|
|
||||||
className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle Button Component - To be used in table cells
|
* Toggle Button Component - To be used in table cells
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> =
|
|||||||
draft: 'warning',
|
draft: 'warning',
|
||||||
review: 'info',
|
review: 'info',
|
||||||
published: 'success',
|
published: 'success',
|
||||||
completed: 'success',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Content() {
|
export default function Content() {
|
||||||
@@ -132,11 +131,23 @@ export default function Content() {
|
|||||||
<div className="font-medium text-gray-900 dark:text-white">
|
<div className="font-medium text-gray-900 dark:text-white">
|
||||||
{item.meta_title || item.title || item.task_title || `Task #${item.task}`}
|
{item.meta_title || item.title || item.task_title || `Task #${item.task}`}
|
||||||
</div>
|
</div>
|
||||||
{item.meta_description && (
|
{(() => {
|
||||||
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
let metaDesc = item.meta_description;
|
||||||
{item.meta_description}
|
// If meta_description is a JSON string, extract the actual value
|
||||||
</div>
|
if (metaDesc && typeof metaDesc === 'string' && metaDesc.trim().startsWith('{')) {
|
||||||
)}
|
try {
|
||||||
|
const parsed = JSON.parse(metaDesc);
|
||||||
|
metaDesc = parsed.meta_description || metaDesc;
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, use as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metaDesc ? (
|
||||||
|
<div className="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||||
|
{metaDesc}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top">
|
<td className="px-5 py-4 align-top">
|
||||||
{item.primary_keyword ? (
|
{item.primary_keyword ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user