Add SEO fields to Tasks model, improve content generation response handling, and enhance progress bar animation
- Added primary_keyword, secondary_keywords, tags, and categories fields to Tasks model - Updated generate_content function to handle full JSON response with all SEO fields - Improved progress bar animation: smooth 1% increments every 300ms - Enhanced step detection for content generation vs clustering vs ideas - Fixed progress modal to show correct messages for each function type - Added comprehensive logging to Keywords and Tasks pages for AI functions - Fixed error handling to show meaningful error messages instead of generic failures
This commit is contained in:
@@ -11,17 +11,16 @@ export default function Usage() {
|
||||
const [limits, setLimits] = useState<LimitCard[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [limitsLoading, setLimitsLoading] = useState(true);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsage();
|
||||
loadLimits();
|
||||
}, [currentPage]);
|
||||
}, []);
|
||||
|
||||
const loadUsage = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetchCreditUsage({ page: currentPage });
|
||||
const response = await fetchCreditUsage({ page: 1 });
|
||||
setUsageLogs(response.results || []);
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load usage logs: ${error.message}`);
|
||||
@@ -48,23 +47,6 @@ export default function Usage() {
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
switch (category) {
|
||||
case 'planner': return 'blue';
|
||||
case 'writer': return 'green';
|
||||
case 'images': return 'purple';
|
||||
case 'ai': return 'orange';
|
||||
case 'general': return 'gray';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageStatus = (percentage: number) => {
|
||||
if (percentage >= 90) return 'danger';
|
||||
if (percentage >= 75) return 'warning';
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const groupedLimits = {
|
||||
planner: limits.filter(l => l.category === 'planner'),
|
||||
writer: limits.filter(l => l.category === 'writer'),
|
||||
@@ -87,14 +69,14 @@ export default function Usage() {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Usage" />
|
||||
<PageMeta title="Usage" description="Monitor your plan limits and usage statistics" />
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Acoount Limit Usage Attemp 6</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Acoount Limits & Usage</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">Monitor your plan limits and usage statistics</p>
|
||||
</div>
|
||||
|
||||
{/* Debug Info - Remove in production */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
{import.meta.env.DEV && (
|
||||
<Card className="p-4 mb-4 bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<strong>Debug:</strong> Loading={limitsLoading ? 'Yes' : 'No'}, Limits={limits.length},
|
||||
|
||||
@@ -54,6 +54,21 @@ export default function Clusters() {
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// AI Function logging state
|
||||
const [aiLogs, setAiLogs] = useState<Array<{
|
||||
timestamp: string;
|
||||
type: 'request' | 'success' | 'error' | 'step';
|
||||
action: string;
|
||||
data: any;
|
||||
stepName?: string;
|
||||
percentage?: number;
|
||||
}>>([]);
|
||||
|
||||
// Track last logged step to avoid duplicates
|
||||
const lastLoggedStepRef = useRef<string | null>(null);
|
||||
const lastLoggedPercentageRef = useRef<number>(-1);
|
||||
const hasReloadedRef = useRef<boolean>(false);
|
||||
|
||||
// Sorting state
|
||||
const [sortBy, setSortBy] = useState<string>('name');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
@@ -201,20 +216,62 @@ export default function Clusters() {
|
||||
// Row action handler
|
||||
const handleRowAction = useCallback(async (action: string, row: Cluster) => {
|
||||
if (action === 'generate_ideas') {
|
||||
const requestData = {
|
||||
ids: [row.id],
|
||||
cluster_name: row.name,
|
||||
cluster_id: row.id,
|
||||
};
|
||||
|
||||
// Log request
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'request',
|
||||
action: 'generate_ideas (Row Action)',
|
||||
data: requestData,
|
||||
}]);
|
||||
|
||||
try {
|
||||
const result = await autoGenerateIdeas([row.id]);
|
||||
|
||||
if (result.success && result.task_id) {
|
||||
// Log success with task_id
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_ideas (Row Action)',
|
||||
data: { task_id: result.task_id, message: result.message },
|
||||
}]);
|
||||
// Async task - show progress modal
|
||||
progressModal.openModal(result.task_id, 'Generating Ideas');
|
||||
} else if (result.success && result.ideas_created) {
|
||||
// Log success with ideas_created
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_ideas (Row Action)',
|
||||
data: { ideas_created: result.ideas_created, message: result.message },
|
||||
}]);
|
||||
// Synchronous completion
|
||||
toast.success(result.message || 'Ideas generated successfully');
|
||||
await loadClusters();
|
||||
} else {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_ideas (Row Action)',
|
||||
data: { error: result.error || 'Failed to generate ideas' },
|
||||
}]);
|
||||
toast.error(result.error || 'Failed to generate ideas');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_ideas (Row Action)',
|
||||
data: { error: error.message || 'Unknown error occurred' },
|
||||
}]);
|
||||
toast.error(`Failed to generate ideas: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -227,33 +284,192 @@ export default function Clusters() {
|
||||
toast.error('Please select at least one cluster to generate ideas');
|
||||
return;
|
||||
}
|
||||
if (ids.length > 5) {
|
||||
toast.error('Maximum 5 clusters allowed for idea generation');
|
||||
if (ids.length > 10) {
|
||||
toast.error('Maximum 10 clusters allowed for idea generation');
|
||||
return;
|
||||
}
|
||||
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const selectedClusters = clusters.filter(c => numIds.includes(c.id));
|
||||
const requestData = {
|
||||
ids: numIds,
|
||||
cluster_count: numIds.length,
|
||||
cluster_names: selectedClusters.map(c => c.name),
|
||||
};
|
||||
|
||||
// Log request
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'request',
|
||||
action: 'auto_generate_ideas (Bulk Action)',
|
||||
data: requestData,
|
||||
}]);
|
||||
|
||||
try {
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const result = await autoGenerateIdeas(numIds);
|
||||
if (result.success) {
|
||||
if (result.task_id) {
|
||||
// Log success with task_id
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'auto_generate_ideas (Bulk Action)',
|
||||
data: { task_id: result.task_id, message: result.message, cluster_count: numIds.length },
|
||||
}]);
|
||||
// Async task - show progress modal
|
||||
progressModal.openModal(result.task_id, 'Generating Content Ideas');
|
||||
// Don't show toast - progress modal will show status
|
||||
} else {
|
||||
// Log success with ideas_created
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'auto_generate_ideas (Bulk Action)',
|
||||
data: { ideas_created: result.ideas_created || 0, message: result.message, cluster_count: numIds.length },
|
||||
}]);
|
||||
// Synchronous completion
|
||||
toast.success(`Ideas generation complete: ${result.ideas_created || 0} ideas created`);
|
||||
await loadClusters();
|
||||
}
|
||||
} else {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'auto_generate_ideas (Bulk Action)',
|
||||
data: { error: result.error || 'Failed to generate ideas', cluster_count: numIds.length },
|
||||
}]);
|
||||
toast.error(result.error || 'Failed to generate ideas');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'auto_generate_ideas (Bulk Action)',
|
||||
data: { error: error.message || 'Unknown error occurred', cluster_count: numIds.length },
|
||||
}]);
|
||||
toast.error(`Failed to generate ideas: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [toast, loadClusters, progressModal]);
|
||||
}, [toast, loadClusters, progressModal, clusters]);
|
||||
|
||||
// Log AI function progress steps
|
||||
useEffect(() => {
|
||||
if (!progressModal.taskId || !progressModal.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = progressModal.progress;
|
||||
const currentStep = progress.details?.phase || '';
|
||||
const currentPercentage = progress.percentage;
|
||||
const currentMessage = progress.message;
|
||||
const currentStatus = progress.status;
|
||||
|
||||
// Log step changes
|
||||
if (currentStep && currentStep !== lastLoggedStepRef.current) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep;
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log percentage changes for same step (if significant change)
|
||||
else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log status changes (error, completed)
|
||||
else if (currentStatus === 'error' || currentStatus === 'completed') {
|
||||
// Only log if we haven't already logged this status for this step
|
||||
if (currentStep !== lastLoggedStepRef.current ||
|
||||
(currentStatus === 'error' && lastLoggedStepRef.current !== 'error') ||
|
||||
(currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) {
|
||||
const stepType = currentStatus === 'error' ? 'error' : 'success';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep || 'Final',
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep || 'Final',
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep || currentStatus;
|
||||
}
|
||||
}
|
||||
}, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title]);
|
||||
|
||||
// Reset step tracking when modal closes or opens
|
||||
useEffect(() => {
|
||||
if (!progressModal.isOpen) {
|
||||
lastLoggedStepRef.current = null;
|
||||
lastLoggedPercentageRef.current = -1;
|
||||
hasReloadedRef.current = false; // Reset reload flag when modal closes
|
||||
} else {
|
||||
// Reset reload flag when modal opens for a new task
|
||||
hasReloadedRef.current = false;
|
||||
}
|
||||
}, [progressModal.isOpen, progressModal.taskId]);
|
||||
|
||||
// Handle modal close - memoized to prevent repeated calls
|
||||
const handleProgressModalClose = useCallback(() => {
|
||||
const wasCompleted = progressModal.progress.status === 'completed';
|
||||
progressModal.closeModal();
|
||||
// Reload data after modal closes (if completed) - only once
|
||||
if (wasCompleted && !hasReloadedRef.current) {
|
||||
hasReloadedRef.current = true;
|
||||
// Use setTimeout to ensure modal is fully closed before reloading
|
||||
setTimeout(() => {
|
||||
loadClusters();
|
||||
// Reset the flag after a delay to allow for future reloads
|
||||
setTimeout(() => {
|
||||
hasReloadedRef.current = false;
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
}, [progressModal.progress.status, progressModal.closeModal, loadClusters]);
|
||||
|
||||
// Close volume dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
@@ -455,16 +671,77 @@ export default function Clusters() {
|
||||
message={progressModal.progress.message}
|
||||
details={progressModal.progress.details}
|
||||
taskId={progressModal.taskId || undefined}
|
||||
onClose={() => {
|
||||
const wasCompleted = progressModal.progress.status === 'completed';
|
||||
progressModal.closeModal();
|
||||
// Reload data after modal closes (if completed)
|
||||
if (wasCompleted) {
|
||||
loadClusters();
|
||||
}
|
||||
}}
|
||||
onClose={handleProgressModalClose}
|
||||
/>
|
||||
|
||||
{/* AI Function Logs - Display below table */}
|
||||
{aiLogs.length > 0 && (
|
||||
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
AI Function Logs
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setAiLogs([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{aiLogs.slice().reverse().map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded border text-xs font-mono ${
|
||||
log.type === 'request'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||
: log.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
|
||||
: log.type === 'error'
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
|
||||
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-semibold ${
|
||||
log.type === 'request'
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: log.type === 'success'
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: log.type === 'error'
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: 'text-purple-700 dark:text-purple-300'
|
||||
}`}>
|
||||
[{log.type.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{log.action}
|
||||
</span>
|
||||
{log.stepName && (
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
{log.stepName}
|
||||
</span>
|
||||
)}
|
||||
{log.percentage !== undefined && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{log.percentage}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<FormModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Link } from "react-router";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { ProgressBar } from "../../components/ui/progress";
|
||||
import { ListIcon, GroupIcon, BoltIcon, PieChartIcon, ArrowRightIcon, CheckCircleIcon, TimeIcon } from "../../icons";
|
||||
|
||||
export default function PlannerDashboard() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Mock data - will be replaced with API calls
|
||||
const stats = {
|
||||
keywords: 245,
|
||||
@@ -175,13 +177,17 @@ export default function PlannerDashboard() {
|
||||
</p>
|
||||
)}
|
||||
{step.status === "pending" && (
|
||||
<Link
|
||||
to={step.path}
|
||||
className="mt-3 inline-block text-xs font-medium text-brand-500 hover:text-brand-600"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(step.path);
|
||||
}}
|
||||
className="mt-3 inline-block text-xs font-medium text-brand-500 hover:text-brand-600 cursor-pointer"
|
||||
>
|
||||
Start Now →
|
||||
</Link>
|
||||
</button>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -89,6 +89,20 @@ export default function Keywords() {
|
||||
const progressModal = useProgressModal();
|
||||
const hasReloadedRef = useRef(false);
|
||||
|
||||
// AI Function Logs state
|
||||
const [aiLogs, setAiLogs] = useState<Array<{
|
||||
timestamp: string;
|
||||
type: 'request' | 'success' | 'error' | 'step';
|
||||
action: string;
|
||||
data: any;
|
||||
stepName?: string;
|
||||
percentage?: number;
|
||||
}>>([]);
|
||||
|
||||
// Track last logged step to avoid duplicates
|
||||
const lastLoggedStepRef = useRef<string | null>(null);
|
||||
const lastLoggedPercentageRef = useRef<number>(-1);
|
||||
|
||||
// Load sectors for active site using sector store
|
||||
useEffect(() => {
|
||||
if (activeSite) {
|
||||
@@ -334,26 +348,68 @@ export default function Keywords() {
|
||||
return;
|
||||
}
|
||||
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const sectorId = activeSector?.id;
|
||||
const selectedKeywords = keywords.filter(k => numIds.includes(k.id));
|
||||
const requestData = {
|
||||
ids: numIds,
|
||||
keyword_count: numIds.length,
|
||||
keyword_names: selectedKeywords.map(k => k.keyword),
|
||||
sector_id: sectorId,
|
||||
};
|
||||
|
||||
// Log request
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'request',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: requestData,
|
||||
}]);
|
||||
|
||||
try {
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const sectorId = activeSector?.id;
|
||||
const result = await autoClusterKeywords(numIds, sectorId);
|
||||
|
||||
// Check if result has success field - if false, it's an error response
|
||||
if (result && result.success === false) {
|
||||
// Error response from API
|
||||
const errorMsg = result.error || 'Failed to cluster keywords';
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: { error: errorMsg, keyword_count: numIds.length },
|
||||
}]);
|
||||
toast.error(errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && result.success) {
|
||||
if (result.task_id) {
|
||||
// Log success with task_id
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: { task_id: result.task_id, message: result.message, keyword_count: numIds.length },
|
||||
}]);
|
||||
// Async task - open progress modal
|
||||
hasReloadedRef.current = false;
|
||||
progressModal.openModal(result.task_id, 'Auto-Clustering Keywords');
|
||||
// Don't show toast - progress modal will show status
|
||||
} else {
|
||||
// Log success with results
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: {
|
||||
clusters_created: result.clusters_created || 0,
|
||||
keywords_updated: result.keywords_updated || 0,
|
||||
keyword_count: numIds.length,
|
||||
message: result.message,
|
||||
},
|
||||
}]);
|
||||
// Synchronous completion
|
||||
toast.success(`Clustering complete: ${result.clusters_created || 0} clusters created, ${result.keywords_updated || 0} keywords updated`);
|
||||
if (!hasReloadedRef.current) {
|
||||
@@ -364,6 +420,13 @@ export default function Keywords() {
|
||||
} else {
|
||||
// Unexpected response format - show error
|
||||
const errorMsg = result?.error || 'Unexpected response format';
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: { error: errorMsg, keyword_count: numIds.length },
|
||||
}]);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -376,12 +439,116 @@ export default function Keywords() {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
}
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'auto_cluster (Bulk Action)',
|
||||
data: { error: errorMsg, keyword_count: numIds.length },
|
||||
}]);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [toast, activeSector, loadKeywords, progressModal]);
|
||||
}, [toast, activeSector, loadKeywords, progressModal, keywords]);
|
||||
|
||||
// Log AI function progress steps
|
||||
useEffect(() => {
|
||||
if (!progressModal.taskId || !progressModal.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = progressModal.progress;
|
||||
const currentStep = progress.details?.phase || '';
|
||||
const currentPercentage = progress.percentage;
|
||||
const currentMessage = progress.message;
|
||||
const currentStatus = progress.status;
|
||||
|
||||
// Log step changes
|
||||
if (currentStep && currentStep !== lastLoggedStepRef.current) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep;
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log percentage changes for same step (if significant change)
|
||||
else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log status changes (error, completed)
|
||||
else if (currentStatus === 'error' || currentStatus === 'completed') {
|
||||
// Only log if we haven't already logged this status for this step
|
||||
if (currentStep !== lastLoggedStepRef.current ||
|
||||
(currentStatus === 'error' && lastLoggedStepRef.current !== 'error') ||
|
||||
(currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) {
|
||||
const stepType = currentStatus === 'error' ? 'error' : 'success';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep || 'Final',
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep || 'Final',
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep || currentStatus;
|
||||
}
|
||||
}
|
||||
}, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title]);
|
||||
|
||||
// Reset step tracking when modal closes or opens
|
||||
useEffect(() => {
|
||||
if (!progressModal.isOpen) {
|
||||
lastLoggedStepRef.current = null;
|
||||
lastLoggedPercentageRef.current = -1;
|
||||
hasReloadedRef.current = false; // Reset reload flag when modal closes
|
||||
} else {
|
||||
// Reset reload flag when modal opens for a new task
|
||||
hasReloadedRef.current = false;
|
||||
}
|
||||
}, [progressModal.isOpen, progressModal.taskId]);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setFormData({
|
||||
@@ -712,6 +879,74 @@ export default function Keywords() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* AI Function Logs - Display below table */}
|
||||
{aiLogs.length > 0 && (
|
||||
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
AI Function Logs
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setAiLogs([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{aiLogs.slice().reverse().map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded border text-xs font-mono ${
|
||||
log.type === 'request'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||
: log.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
|
||||
: log.type === 'error'
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
|
||||
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-semibold ${
|
||||
log.type === 'request'
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: log.type === 'success'
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: log.type === 'error'
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: 'text-purple-700 dark:text-purple-300'
|
||||
}`}>
|
||||
[{log.type.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{log.action}
|
||||
</span>
|
||||
{log.stepName && (
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
{log.stepName}
|
||||
</span>
|
||||
)}
|
||||
{log.percentage !== undefined && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{log.percentage}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Consistent with Keywords page layout, structure and design
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||
import {
|
||||
fetchTasks,
|
||||
@@ -75,6 +75,21 @@ export default function Tasks() {
|
||||
// Progress modal for AI functions
|
||||
const progressModal = useProgressModal();
|
||||
|
||||
// AI Function Logs state
|
||||
const [aiLogs, setAiLogs] = useState<Array<{
|
||||
timestamp: string;
|
||||
type: 'request' | 'success' | 'error' | 'step';
|
||||
action: string;
|
||||
data: any;
|
||||
stepName?: string;
|
||||
percentage?: number;
|
||||
}>>([]);
|
||||
|
||||
// Track last logged step to avoid duplicates
|
||||
const lastLoggedStepRef = useRef<string | null>(null);
|
||||
const lastLoggedPercentageRef = useRef<number>(-1);
|
||||
const hasReloadedRef = useRef<boolean>(false);
|
||||
|
||||
// Load clusters for filter dropdown
|
||||
useEffect(() => {
|
||||
const loadClusters = async () => {
|
||||
@@ -208,23 +223,65 @@ export default function Tasks() {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const requestData = {
|
||||
ids: [row.id],
|
||||
task_title: row.title,
|
||||
task_id: row.id,
|
||||
};
|
||||
|
||||
// Log request
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'request',
|
||||
action: 'generate_content (Row Action)',
|
||||
data: requestData,
|
||||
}]);
|
||||
|
||||
try {
|
||||
const result = await autoGenerateContent([row.id]);
|
||||
|
||||
if (result.success) {
|
||||
if (result.task_id) {
|
||||
// Log success with task_id
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_content (Row Action)',
|
||||
data: { task_id: result.task_id, message: result.message },
|
||||
}]);
|
||||
// Async task - show progress modal
|
||||
progressModal.openModal(result.task_id, 'Generating Content');
|
||||
toast.success('Content generation started');
|
||||
} else {
|
||||
// Log success with results
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_content (Row Action)',
|
||||
data: { tasks_updated: result.tasks_updated || 0, message: result.message },
|
||||
}]);
|
||||
// Synchronous completion
|
||||
toast.success(`Content generated successfully: ${result.tasks_updated || 0} article generated`);
|
||||
await loadTasks();
|
||||
}
|
||||
} else {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_content (Row Action)',
|
||||
data: { error: result.error || 'Failed to generate content' },
|
||||
}]);
|
||||
toast.error(result.error || 'Failed to generate content');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_content (Row Action)',
|
||||
data: { error: error.message || 'Unknown error occurred' },
|
||||
}]);
|
||||
toast.error(`Failed to generate content: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -242,29 +299,169 @@ export default function Tasks() {
|
||||
toast.error('Maximum 10 tasks allowed for image generation');
|
||||
return;
|
||||
}
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const selectedTasks = tasks.filter(t => numIds.includes(t.id));
|
||||
const requestData = {
|
||||
ids: numIds,
|
||||
task_count: numIds.length,
|
||||
task_titles: selectedTasks.map(t => t.title),
|
||||
};
|
||||
|
||||
// Log request
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'request',
|
||||
action: 'generate_images (Bulk Action)',
|
||||
data: requestData,
|
||||
}]);
|
||||
|
||||
try {
|
||||
const numIds = ids.map(id => parseInt(id));
|
||||
const result = await autoGenerateImages(numIds);
|
||||
if (result.success) {
|
||||
if (result.task_id) {
|
||||
// Log success with task_id
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_images (Bulk Action)',
|
||||
data: { task_id: result.task_id, message: result.message, task_count: numIds.length },
|
||||
}]);
|
||||
// Async task - show progress modal
|
||||
progressModal.openModal(result.task_id, 'Generating Images');
|
||||
toast.success('Image generation started');
|
||||
} else {
|
||||
// Log success with results
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'success',
|
||||
action: 'generate_images (Bulk Action)',
|
||||
data: { images_created: result.images_created || 0, message: result.message, task_count: numIds.length },
|
||||
}]);
|
||||
// Synchronous completion
|
||||
toast.success(`Image generation complete: ${result.images_created || 0} images generated`);
|
||||
await loadTasks();
|
||||
}
|
||||
} else {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_images (Bulk Action)',
|
||||
data: { error: result.error || 'Failed to generate images', task_count: numIds.length },
|
||||
}]);
|
||||
toast.error(result.error || 'Failed to generate images');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Log error
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'error',
|
||||
action: 'generate_images (Bulk Action)',
|
||||
data: { error: error.message || 'Unknown error occurred', task_count: numIds.length },
|
||||
}]);
|
||||
toast.error(`Failed to generate images: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [toast, loadTasks, progressModal]);
|
||||
}, [toast, loadTasks, progressModal, tasks]);
|
||||
|
||||
// Log AI function progress steps
|
||||
useEffect(() => {
|
||||
if (!progressModal.taskId || !progressModal.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = progressModal.progress;
|
||||
const currentStep = progress.details?.phase || '';
|
||||
const currentPercentage = progress.percentage;
|
||||
const currentMessage = progress.message;
|
||||
const currentStatus = progress.status;
|
||||
|
||||
// Log step changes
|
||||
if (currentStep && currentStep !== lastLoggedStepRef.current) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep;
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log percentage changes for same step (if significant change)
|
||||
else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) {
|
||||
const stepType = currentStatus === 'error' ? 'error' :
|
||||
currentStatus === 'completed' ? 'success' : 'step';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep,
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep,
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedPercentageRef.current = currentPercentage;
|
||||
}
|
||||
// Log status changes (error, completed)
|
||||
else if (currentStatus === 'error' || currentStatus === 'completed') {
|
||||
// Only log if we haven't already logged this status for this step
|
||||
if (currentStep !== lastLoggedStepRef.current ||
|
||||
(currentStatus === 'error' && lastLoggedStepRef.current !== 'error') ||
|
||||
(currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) {
|
||||
const stepType = currentStatus === 'error' ? 'error' : 'success';
|
||||
|
||||
setAiLogs(prev => [...prev, {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: stepType,
|
||||
action: progressModal.title || 'AI Function',
|
||||
stepName: currentStep || 'Final',
|
||||
percentage: currentPercentage,
|
||||
data: {
|
||||
step: currentStep || 'Final',
|
||||
message: currentMessage,
|
||||
percentage: currentPercentage,
|
||||
status: currentStatus,
|
||||
details: progress.details,
|
||||
},
|
||||
}]);
|
||||
|
||||
lastLoggedStepRef.current = currentStep || currentStatus;
|
||||
}
|
||||
}
|
||||
}, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title]);
|
||||
|
||||
// Reset step tracking when modal closes or opens
|
||||
useEffect(() => {
|
||||
if (!progressModal.isOpen) {
|
||||
lastLoggedStepRef.current = null;
|
||||
lastLoggedPercentageRef.current = -1;
|
||||
hasReloadedRef.current = false; // Reset reload flag when modal closes
|
||||
} else {
|
||||
// Reset reload flag when modal opens for a new task
|
||||
hasReloadedRef.current = false;
|
||||
}
|
||||
}, [progressModal.isOpen, progressModal.taskId]);
|
||||
|
||||
// Create page config
|
||||
const pageConfig = useMemo(() => {
|
||||
@@ -442,12 +639,84 @@ export default function Tasks() {
|
||||
const wasCompleted = progressModal.progress.status === 'completed';
|
||||
progressModal.closeModal();
|
||||
// Reload data after modal closes (if completed)
|
||||
if (wasCompleted) {
|
||||
if (wasCompleted && !hasReloadedRef.current) {
|
||||
hasReloadedRef.current = true;
|
||||
loadTasks();
|
||||
setTimeout(() => {
|
||||
hasReloadedRef.current = false;
|
||||
}, 1000);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* AI Function Logs - Display below table */}
|
||||
{aiLogs.length > 0 && (
|
||||
<div className="mt-6 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
AI Function Logs
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setAiLogs([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{aiLogs.slice().reverse().map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded border text-xs font-mono ${
|
||||
log.type === 'request'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800'
|
||||
: log.type === 'success'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
|
||||
: log.type === 'error'
|
||||
? 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
|
||||
: 'bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`font-semibold ${
|
||||
log.type === 'request'
|
||||
? 'text-blue-700 dark:text-blue-300'
|
||||
: log.type === 'success'
|
||||
? 'text-green-700 dark:text-green-300'
|
||||
: log.type === 'error'
|
||||
? 'text-red-700 dark:text-red-300'
|
||||
: 'text-purple-700 dark:text-purple-300'
|
||||
}`}>
|
||||
[{log.type.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{log.action}
|
||||
</span>
|
||||
{log.stepName && (
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
{log.stepName}
|
||||
</span>
|
||||
)}
|
||||
{log.percentage !== undefined && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{log.percentage}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{new Date(log.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<FormModal
|
||||
isOpen={isModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user