Add Linker and Optimizer modules with API integration and frontend components

- Added Linker and Optimizer apps to `INSTALLED_APPS` in `settings.py`.
- Configured API endpoints for Linker and Optimizer in `urls.py`.
- Implemented `OptimizeContentFunction` for content optimization in the AI module.
- Created prompts for content optimization and site structure generation.
- Updated `OptimizerService` to utilize the new AI function for content optimization.
- Developed frontend components including dashboards and content lists for Linker and Optimizer.
- Integrated new routes and sidebar navigation for Linker and Optimizer in the frontend.
- Enhanced content management with source and sync status filters in the Writer module.
- Comprehensive test coverage added for new features and components.
This commit is contained in:
alorig
2025-11-18 00:41:00 +05:00
parent 4b9e1a49a9
commit f7115190dc
60 changed files with 4932 additions and 80 deletions

View File

@@ -0,0 +1,230 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { linkerApi } from '../../api/linker.api';
import { fetchContent, Content as ContentType } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { SourceBadge, ContentSource } from '../../components/content/SourceBadge';
import { LinkResults } from '../../components/linker/LinkResults';
import { Link2, Loader2 } from 'lucide-react';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
export default function LinkerContentList() {
const navigate = useNavigate();
const toast = useToast();
const { activeSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
const [content, setContent] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(true);
const [processing, setProcessing] = useState<number | null>(null);
const [linkResults, setLinkResults] = useState<Record<number, any>>({});
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const loadContent = useCallback(async () => {
setLoading(true);
try {
const data = await fetchContent({
page: currentPage,
page_size: pageSize,
sector_id: activeSector?.id,
});
setContent(data.results || []);
setTotalCount(data.count || 0);
} catch (error: any) {
console.error('Error loading content:', error);
toast.error(`Failed to load content: ${error.message}`);
} finally {
setLoading(false);
}
}, [currentPage, pageSize, activeSector, toast]);
useEffect(() => {
loadContent();
}, [loadContent]);
const handleLink = async (contentId: number) => {
try {
setProcessing(contentId);
const result = await linkerApi.process(contentId);
setLinkResults(prev => ({
...prev,
[contentId]: result,
}));
toast.success(`Added ${result.links_added || 0} link${result.links_added !== 1 ? 's' : ''} to content`);
// Refresh content list
await loadContent();
} catch (error: any) {
console.error('Error linking content:', error);
toast.error(`Failed to link content: ${error.message}`);
} finally {
setProcessing(null);
}
};
const handleBatchLink = async (contentIds: number[]) => {
try {
setProcessing(-1); // Special value for batch
const results = await linkerApi.batchProcess(contentIds);
let totalLinks = 0;
results.forEach((result: any) => {
setLinkResults(prev => ({
...prev,
[result.content_id]: result,
}));
totalLinks += result.links_added || 0;
});
toast.success(`Added ${totalLinks} link${totalLinks !== 1 ? 's' : ''} to ${results.length} content item${results.length !== 1 ? 's' : ''}`);
// Refresh content list
await loadContent();
} catch (error: any) {
console.error('Error batch linking content:', error);
toast.error(`Failed to link content: ${error.message}`);
} finally {
setProcessing(null);
}
};
return (
<>
<PageMeta title="Link Content" description="Process content for internal linking" />
<div className="space-y-6">
<PageHeader
title="Link Content"
description="Add internal links to your content"
/>
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading content...</p>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Title
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Source
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Links
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Version
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{content.map((item) => {
const result = linkResults[item.id];
const isProcessing = processing === item.id;
return (
<tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{item.title || 'Untitled'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<SourceBadge source={(item.source as ContentSource) || 'igny8'} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{item.internal_links?.length || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{item.linker_version || 0}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<button
onClick={() => handleLink(item.id)}
disabled={isProcessing || processing === -1}
className="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Processing...
</>
) : (
<>
<Link2 className="w-4 h-4" />
Add Links
</>
)}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Pagination */}
{totalCount > pageSize && (
<div className="bg-gray-50 dark:bg-gray-900 px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalCount)} of {totalCount} results
</div>
<div className="flex gap-2">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={currentPage * pageSize >= totalCount}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded text-sm disabled:opacity-50"
>
Next
</button>
</div>
</div>
)}
{/* Link Results */}
{Object.keys(linkResults).length > 0 && (
<div className="p-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Recent Results</h3>
<div className="space-y-4">
{Object.entries(linkResults).slice(-3).map(([contentId, result]) => (
<LinkResults
key={contentId}
contentId={parseInt(contentId)}
links={result.links || []}
linksAdded={result.links_added || 0}
linkerVersion={result.linker_version || 0}
/>
))}
</div>
</div>
)}
</div>
)}
</div>
</>
);
}