|
|
|
@ -12,9 +12,7 @@ import { |
|
|
|
Play, |
|
|
|
X, |
|
|
|
AlertTriangle, |
|
|
|
RefreshCw, |
|
|
|
ExternalLink, |
|
|
|
Info |
|
|
|
ExternalLink |
|
|
|
} from 'lucide-react'; |
|
|
|
|
|
|
|
function App() { |
|
|
|
@ -137,8 +135,6 @@ function App() { |
|
|
|
const fetchLogs = async () => { |
|
|
|
setLoadingLogs(true); |
|
|
|
try { |
|
|
|
// In Django REST, runs are queried per-crawler. We fetch all crawlers, |
|
|
|
// then request runs for each and merge/sort them. |
|
|
|
const crawlersList = crawlers.length > 0 ? crawlers : (await axios.get('/api/crawlers/')).data; |
|
|
|
const promises = crawlersList.map(async (c) => { |
|
|
|
try { |
|
|
|
@ -256,7 +252,7 @@ function App() { |
|
|
|
|
|
|
|
const triggerCrawler = async (id) => { |
|
|
|
try { |
|
|
|
const res = await axios.post(`/api/crawlers/${id}/trigger/`); |
|
|
|
await axios.post(`/api/crawlers/${id}/trigger/`); |
|
|
|
showToast('درخواست اجرای کرالر با موفقیت ارسال شد', 'success'); |
|
|
|
if (activeTab === 'logs') { |
|
|
|
fetchLogs(); |
|
|
|
@ -289,75 +285,68 @@ function App() { |
|
|
|
}; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className="app-container"> |
|
|
|
<div className="app-container flex flex-col md:flex-row min-h-screen relative font-fa"> |
|
|
|
|
|
|
|
{/* Toast Notification */} |
|
|
|
{toast && ( |
|
|
|
<div style={{ |
|
|
|
position: 'fixed', |
|
|
|
top: '20px', |
|
|
|
left: '20px', |
|
|
|
backgroundColor: toast.type === 'success' ? 'var(--success)' : toast.type === 'danger' ? 'var(--danger)' : 'var(--accent)', |
|
|
|
color: 'white', |
|
|
|
padding: '10px 20px', |
|
|
|
borderRadius: '8px', |
|
|
|
zIndex: 9999, |
|
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', |
|
|
|
direction: 'rtl', |
|
|
|
animation: 'fadeIn 0.2s ease' |
|
|
|
}}> |
|
|
|
<div |
|
|
|
className="fixed top-5 left-5 px-5 py-3 rounded-lg z-50 shadow-2xl text-white direction-rtl animate-fade-in" |
|
|
|
style={{ |
|
|
|
backgroundColor: toast.type === 'success' ? '#10b981' : toast.type === 'danger' ? '#ef4444' : '#a855f7', |
|
|
|
}} |
|
|
|
> |
|
|
|
{toast.message} |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* Sidebar */} |
|
|
|
<aside class="sidebar"> |
|
|
|
<div class="sidebar-brand"> |
|
|
|
<div class="brand-logo"> |
|
|
|
<ShieldAlert className="brand-icon" /> |
|
|
|
<aside className="w-full md:w-72 bg-slate-900 border-l border-white/5 flex flex-col p-6 sticky top-0 md:h-screen z-10 shrink-0"> |
|
|
|
<div className="flex items-center gap-3 pb-4 mb-10 border-b border-white/5"> |
|
|
|
<div className="bg-gradient-to-br from-purple-500 to-blue-500 w-11 h-11 rounded-xl flex items-center justify-center shadow-[0_0_15px_rgba(168,85,247,0.3)] text-white"> |
|
|
|
<ShieldAlert className="w-6 h-6" /> |
|
|
|
</div> |
|
|
|
<div class="brand-info"> |
|
|
|
<h1>دیدبان دیوار</h1> |
|
|
|
<span>کرال هوشمند با AI</span> |
|
|
|
<div className="flex flex-col"> |
|
|
|
<h1 className="text-lg font-bold text-white leading-tight">دیدبان دیوار</h1> |
|
|
|
<span className="text-[10px] text-gray-500">کرال هوشمند با AI</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<nav class="sidebar-nav"> |
|
|
|
<nav className="flex flex-row md:flex-col overflow-x-auto md:overflow-x-visible gap-2 flex-grow pb-2 md:pb-0"> |
|
|
|
<button |
|
|
|
className={`nav-item ${activeTab === 'crawlers' ? 'active' : ''}`} |
|
|
|
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'crawlers' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`} |
|
|
|
onClick={() => switchTab('crawlers')} |
|
|
|
> |
|
|
|
<Layers /> |
|
|
|
<Layers className="w-5 h-5" /> |
|
|
|
<span>مدیریت کرالرها</span> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
className={`nav-item ${activeTab === 'ads' ? 'active' : ''}`} |
|
|
|
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'ads' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`} |
|
|
|
onClick={() => switchTab('ads')} |
|
|
|
> |
|
|
|
<Newspaper /> |
|
|
|
<Newspaper className="w-5 h-5" /> |
|
|
|
<span>فید آگهیها</span> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
className={`nav-item ${activeTab === 'logs' ? 'active' : ''}`} |
|
|
|
className={`w-auto md:w-full bg-transparent border-none text-gray-400 flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer text-sm font-medium transition-all duration-200 text-right hover:bg-white/5 hover:text-white shrink-0 ${activeTab === 'logs' ? 'bg-gradient-to-r from-purple-500/15 to-transparent border-r-3 border-purple-500 text-purple-400 font-bold' : ''}`} |
|
|
|
onClick={() => switchTab('logs')} |
|
|
|
> |
|
|
|
<History /> |
|
|
|
<History className="w-5 h-5" /> |
|
|
|
<span>تاریخچه اجرا</span> |
|
|
|
</button> |
|
|
|
</nav> |
|
|
|
|
|
|
|
<div class="sidebar-footer"> |
|
|
|
<span class="version-label">نسخه ۱.۰.۰ (MVP)</span> |
|
|
|
<div className="pt-4 border-t border-white/5 text-center hidden md:block"> |
|
|
|
<span className="text-[11px] text-gray-500 font-en">نسخه ۱.۰.۰ (MVP)</span> |
|
|
|
</div> |
|
|
|
</aside> |
|
|
|
|
|
|
|
{/* Main Content */} |
|
|
|
<div className="main-content"> |
|
|
|
<div className="flex-grow flex flex-col min-w-0"> |
|
|
|
|
|
|
|
{/* Header */} |
|
|
|
<header className="app-header"> |
|
|
|
<header className="bg-slate-950/70 backdrop-blur-md border-b border-white/5 flex items-center justify-between px-6 md:px-8 py-5 sticky top-0 z-9"> |
|
|
|
<div className="header-title"> |
|
|
|
<h2> |
|
|
|
<h2 className="text-lg font-bold text-white"> |
|
|
|
{activeTab === 'crawlers' && 'مدیریت کرالرها'} |
|
|
|
{activeTab === 'ads' && 'فید آگهیهای دیوار'} |
|
|
|
{activeTab === 'logs' && 'تاریخچه اجرای کرالرها'} |
|
|
|
@ -365,13 +354,13 @@ function App() { |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="header-actions"> |
|
|
|
<div className="system-health"> |
|
|
|
<div className="health-item"> |
|
|
|
<span className={`dot ${health.database === 'healthy' ? 'green' : 'red'}`}></span> |
|
|
|
<div className="flex items-center gap-5 bg-white/2 px-4 py-2 rounded-full border border-white/5 text-xs text-gray-400"> |
|
|
|
<div className="flex items-center gap-1.5"> |
|
|
|
<span className={`w-2 h-2 rounded-full inline-block ${health.database === 'healthy' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-rose-500 shadow-[0_0_8px_#ef4444]'}`}></span> |
|
|
|
<span>دیتابیس</span> |
|
|
|
</div> |
|
|
|
<div className="health-item"> |
|
|
|
<span className={`dot ${health.redis === 'healthy' ? 'green' : 'red'}`}></span> |
|
|
|
<div className="flex items-center gap-1.5"> |
|
|
|
<span className={`w-2 h-2 rounded-full inline-block ${health.redis === 'healthy' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : 'bg-rose-500 shadow-[0_0_8px_#ef4444]'}`}></span> |
|
|
|
<span>ردیس</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -379,84 +368,85 @@ function App() { |
|
|
|
</header> |
|
|
|
|
|
|
|
{/* Viewport */} |
|
|
|
<main className="view-viewport"> |
|
|
|
<main className="p-6 md:p-8 flex-grow"> |
|
|
|
|
|
|
|
{/* TAB 1: Crawlers View */} |
|
|
|
{activeTab === 'crawlers' && ( |
|
|
|
<div className="tab-view active"> |
|
|
|
<div className="view-actions-bar"> |
|
|
|
<button className="btn btn-primary" onClick={() => openCrawlerModal()}> |
|
|
|
<Plus /> |
|
|
|
<div className="animate-fade-in"> |
|
|
|
<div className="flex justify-between items-center mb-8"> |
|
|
|
<button className="inline-flex items-center justify-center gap-2 border border-transparent rounded-lg px-4 py-2.5 text-sm font-medium cursor-pointer transition-all duration-200 bg-purple-500 text-white shadow-[0_4px_14px_rgba(168,85,247,0.3)] hover:bg-purple-600 hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(168,85,247,0.4)]" onClick={() => openCrawlerModal()}> |
|
|
|
<Plus className="w-5 h-5" /> |
|
|
|
<span>تعریف کرالر جدید</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
{loadingCrawlers ? ( |
|
|
|
<div className="loading-state"> |
|
|
|
<div className="spinner"></div> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div> |
|
|
|
<p>در حال بارگذاری کرالرها...</p> |
|
|
|
</div> |
|
|
|
) : crawlers.length === 0 ? ( |
|
|
|
<div className="empty-state"> |
|
|
|
<AlertTriangle /> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" /> |
|
|
|
<p>هیچ کرالری تعریف نشده است. جهت پایش دیوار، یک کرالر جدید ایجاد کنید.</p> |
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
<div className="crawlers-grid"> |
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
|
|
|
{crawlers.map(c => ( |
|
|
|
<div className="card" key={c.id}> |
|
|
|
<div className="card-header"> |
|
|
|
<h4 className="card-title" title={c.title}>{c.title}</h4> |
|
|
|
<div className="toggle-group"> |
|
|
|
<label className="switch-container"> |
|
|
|
<div className="bg-slate-900/40 backdrop-blur-md border border-white/5 rounded-2xl p-6 shadow-xl hover:bg-slate-900/60 hover:border-purple-500/40 transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl flex flex-col relative overflow-hidden" key={c.id}> |
|
|
|
<div className="flex justify-between items-start mb-4 gap-4"> |
|
|
|
<h4 className="text-lg font-bold text-white overflow-hidden text-overflow-ellipsis whitespace-nowrap max-w-[200px]" title={c.title}>{c.title}</h4> |
|
|
|
<div className="flex items-center gap-3"> |
|
|
|
<label className="relative inline-flex items-center cursor-pointer"> |
|
|
|
<input |
|
|
|
type="checkbox" |
|
|
|
checked={c.is_active} |
|
|
|
onChange={() => toggleCrawlerActive(c)} |
|
|
|
className="sr-only peer" |
|
|
|
/> |
|
|
|
<span className="switch-slider"></span> |
|
|
|
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="card-body"> |
|
|
|
<div className="card-field"> |
|
|
|
<span className="card-field-label">لینک فیلتر دیوار</span> |
|
|
|
<span className="card-field-value url" title={c.divar_url}>{c.divar_url}</span> |
|
|
|
<div className="flex-grow flex flex-col gap-3 mb-5"> |
|
|
|
<div className="flex flex-col gap-1"> |
|
|
|
<span className="text-[11px] text-gray-500">لینک فیلتر دیوار</span> |
|
|
|
<span className="text-xs text-blue-400 font-en direction-ltr break-all overflow-hidden text-ellipsis whitespace-nowrap block" title={c.divar_url}>{c.divar_url}</span> |
|
|
|
</div> |
|
|
|
<div className="card-field"> |
|
|
|
<span className="card-field-label">پرامپت ارزیابی AI</span> |
|
|
|
<span className="card-field-value prompt" title={c.detection_prompt}>{c.detection_prompt}</span> |
|
|
|
<div className="flex flex-col gap-1"> |
|
|
|
<span className="text-[11px] text-gray-500">پرامپت ارزیابی AI</span> |
|
|
|
<span className="text-[13px] text-gray-300 line-clamp-2" title={c.detection_prompt}>{c.detection_prompt}</span> |
|
|
|
</div> |
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}> |
|
|
|
<div className="card-field"> |
|
|
|
<span className="card-field-label">فاصله زمانی پایش</span> |
|
|
|
<span className="card-field-value arabic-num-font">{c.interval_minutes} دقیقه</span> |
|
|
|
<div className="grid grid-cols-2 gap-3.5"> |
|
|
|
<div className="flex flex-col gap-1"> |
|
|
|
<span className="text-[11px] text-gray-500">فاصله زمانی پایش</span> |
|
|
|
<span className="text-xs text-gray-300 font-en">{c.interval_minutes}m</span> |
|
|
|
</div> |
|
|
|
<div className="card-field"> |
|
|
|
<span className="card-field-label">بازه زمانی مجاز</span> |
|
|
|
<span className="card-field-value arabic-num-font">{c.start_hour.substring(0,5)} الی {c.end_hour.substring(0,5)}</span> |
|
|
|
<div className="flex flex-col gap-1"> |
|
|
|
<span className="text-[11px] text-gray-500">بازه زمانی مجاز</span> |
|
|
|
<span className="text-xs text-gray-300 font-en">{c.start_hour.substring(0,5)} - {c.end_hour.substring(0,5)}</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
{c.telegram_channel_id && ( |
|
|
|
<div className="card-field"> |
|
|
|
<span className="card-field-label">کانال اعلانات تلگرام</span> |
|
|
|
<span className="card-field-value font-mono">{c.telegram_channel_id}</span> |
|
|
|
<div className="flex flex-col gap-1"> |
|
|
|
<span className="text-[11px] text-gray-500">کانال اعلانات تلگرام</span> |
|
|
|
<span className="text-xs text-purple-400 font-mono">{c.telegram_channel_id}</span> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="card-footer"> |
|
|
|
<button className="btn btn-secondary btn-icon-only" title="اجرای فوری" onClick={() => triggerCrawler(c.id)}> |
|
|
|
<Play style={{ fill: 'currentColor' }} /> |
|
|
|
<div className="flex justify-between items-center border-t border-white/5 pt-4 mt-auto"> |
|
|
|
<button className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-white hover:bg-white/10" title="اجرای فوری" onClick={() => triggerCrawler(c.id)}> |
|
|
|
<Play className="w-5 h-5 fill-current" /> |
|
|
|
</button> |
|
|
|
|
|
|
|
<div className="card-actions"> |
|
|
|
<button className="btn btn-secondary btn-icon-only" title="ویرایش" onClick={() => openCrawlerModal(c)}> |
|
|
|
<Edit2 /> |
|
|
|
<div className="flex items-center gap-2"> |
|
|
|
<button className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-white hover:bg-white/10" title="ویرایش" onClick={() => openCrawlerModal(c)}> |
|
|
|
<Edit2 className="w-4 h-4" /> |
|
|
|
</button> |
|
|
|
<button className="btn btn-danger btn-icon-only" title="حذف" onClick={() => deleteCrawler(c.id)}> |
|
|
|
<Trash2 /> |
|
|
|
<button className="inline-flex items-center justify-center border border-transparent rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-rose-500 text-white shadow-[0_4px_14px_rgba(239,68,68,0.2)] hover:bg-rose-600 hover:-translate-y-0.5" title="حذف" onClick={() => deleteCrawler(c.id)}> |
|
|
|
<Trash2 className="w-4 h-4" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -469,22 +459,27 @@ function App() { |
|
|
|
|
|
|
|
{/* TAB 2: Ads View */} |
|
|
|
{activeTab === 'ads' && ( |
|
|
|
<div className="tab-view active"> |
|
|
|
<div className="ads-filters-bar"> |
|
|
|
<div className="search-box"> |
|
|
|
<Search className="search-icon" /> |
|
|
|
<div className="animate-fade-in"> |
|
|
|
<div className="flex flex-col lg:flex-row justify-between gap-5 bg-slate-900/40 p-5 rounded-2xl border border-white/5 mb-8 items-center"> |
|
|
|
<div className="relative w-full lg:max-w-md"> |
|
|
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-gray-500" /> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
placeholder="جستجو در فید آگهیها..." |
|
|
|
value={searchTerm} |
|
|
|
onChange={(e) => setSearchTerm(e.target.value)} |
|
|
|
className="w-full bg-black/20 border border-white/5 rounded-lg py-2.5 pr-10 pl-4 text-white text-sm outline-none transition-all duration-200 focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.15)]" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="filters-group"> |
|
|
|
<div className="filter-item"> |
|
|
|
<div className="flex items-center gap-6 flex-wrap w-full lg:w-auto justify-between lg:justify-end"> |
|
|
|
<div className="flex items-center gap-2 text-sm text-gray-300"> |
|
|
|
<label>فیلتر کرالر:</label> |
|
|
|
<select value={filterTaskId} onChange={(e) => setFilterTaskId(e.target.value)}> |
|
|
|
<select |
|
|
|
value={filterTaskId} |
|
|
|
onChange={(e) => setFilterTaskId(e.target.value)} |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500" |
|
|
|
> |
|
|
|
<option value="">همه کرالرها</option> |
|
|
|
{crawlers.map(c => ( |
|
|
|
<option key={c.id} value={c.id}>{c.title}</option> |
|
|
|
@ -492,69 +487,70 @@ function App() { |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="filter-item toggle-filter"> |
|
|
|
<label className="switch-container"> |
|
|
|
<div className="flex items-center gap-3"> |
|
|
|
<label className="relative inline-flex items-center cursor-pointer"> |
|
|
|
<input |
|
|
|
type="checkbox" |
|
|
|
checked={filterOnlyFlagged} |
|
|
|
onChange={(e) => setFilterOnlyFlagged(e.target.checked)} |
|
|
|
className="sr-only peer" |
|
|
|
/> |
|
|
|
<span class="switch-slider"></span> |
|
|
|
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div> |
|
|
|
</label> |
|
|
|
<span className="toggle-label">فقط پرچمگذاری شده (AI)</span> |
|
|
|
<span className="text-sm text-gray-300">فقط پرچمگذاری شده (AI)</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{loadingAds ? ( |
|
|
|
<div className="loading-state"> |
|
|
|
<div className="spinner"></div> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div> |
|
|
|
<p>در حال بارگذاری آگهیها...</p> |
|
|
|
</div> |
|
|
|
) : getFilteredAds().length === 0 ? ( |
|
|
|
<div className="empty-state"> |
|
|
|
<AlertTriangle /> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" /> |
|
|
|
<p>هیچ آگهی با معیارهای فیلتر یافت نشد.</p> |
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
<div className="ads-grid"> |
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
|
{getFilteredAds().map(adEval => ( |
|
|
|
<div className={`card ad-card ${adEval.is_flagged ? 'flagged' : ''}`} key={adEval.id}> |
|
|
|
<div className="ad-meta-top"> |
|
|
|
<span className="ad-published-at">{formatDate(adEval.evaluated_at)}</span> |
|
|
|
<div className={`bg-slate-900/40 backdrop-blur-md rounded-2xl p-6 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl flex flex-col relative overflow-hidden border-t-4 ${adEval.is_flagged ? 'border-purple-500 bg-gradient-to-b from-purple-500/3 to-slate-900/40 shadow-[0_8px_32px_rgba(168,85,247,0.05)]' : 'border-white/5'}`} key={adEval.id}> |
|
|
|
<div className="flex justify-between items-center mb-2 w-full text-xs text-gray-500"> |
|
|
|
<span className="font-en">{formatDate(adEval.evaluated_at)}</span> |
|
|
|
{adEval.ad.price && ( |
|
|
|
<span className="ad-price">{adEval.ad.price}</span> |
|
|
|
<span className="text-[13px] font-bold text-purple-400">{adEval.ad.price}</span> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<h4 className="card-title" style={{ maxWidth: '100%', marginBottom: '0.5rem' }}> |
|
|
|
<h4 className="text-base font-bold text-white mb-2 leading-snug"> |
|
|
|
{adEval.ad.title} |
|
|
|
</h4> |
|
|
|
|
|
|
|
<p className="ad-desc">{adEval.ad.description}</p> |
|
|
|
<p className="text-xs.5 text-gray-400 line-clamp-3 mb-4 leading-relaxed break-all">{adEval.ad.description}</p> |
|
|
|
|
|
|
|
{adEval.is_flagged && ( |
|
|
|
<div className="ai-flag-box"> |
|
|
|
<div className="ai-flag-header"> |
|
|
|
<div className="bg-purple-500/8 border border-purple-500/20 rounded-xl p-4 text-sm mb-4"> |
|
|
|
<div className="flex items-center justify-between text-purple-400 font-bold mb-1.5"> |
|
|
|
<span>تحلیل هوش مصنوعی (پرچمگذاری شده)</span> |
|
|
|
{adEval.confidence && ( |
|
|
|
<span className="ai-confidence-badge">اطمینان: {Math.round(adEval.confidence * 100)}٪</span> |
|
|
|
<span className="text-[11px] bg-purple-500/25 px-1.5 py-0.5 rounded font-en">Confidence: {Math.round(adEval.confidence * 100)}%</span> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
<p className="ai-flag-reason">{adEval.reason}</p> |
|
|
|
<p className="text-gray-300 text-xs.5 leading-relaxed">{adEval.reason}</p> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
<div className="ad-tag-list"> |
|
|
|
<span className="ad-tag">توکن: {adEval.ad.divar_token}</span> |
|
|
|
<div className="flex flex-wrap gap-2 mb-4"> |
|
|
|
<span className="bg-white/2 border border-white/5 px-2 py-0.5 rounded text-[11px] text-gray-400 font-en">Token: {adEval.ad.divar_token}</span> |
|
|
|
{adEval.ad.category && ( |
|
|
|
<span className="ad-tag">{adEval.ad.category}</span> |
|
|
|
<span className="bg-white/2 border border-white/5 px-2 py-0.5 rounded text-[11px] text-gray-400">{adEval.ad.category}</span> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="card-footer" style={{ borderTop: '1px solid var(--border-color)', paddingTop: '1rem', marginTop: 'auto' }}> |
|
|
|
<a href={adEval.ad.url} target="_blank" rel="noopener noreferrer" className="btn btn-secondary btn-icon-only" title="مشاهده در دیوار" style={{ color: 'var(--info)' }}> |
|
|
|
<ExternalLink /> |
|
|
|
<div className="flex justify-between items-center border-t border-white/5 pt-4 mt-auto"> |
|
|
|
<a href={adEval.ad.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center border rounded-lg p-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 border-white/5 text-blue-400 hover:bg-white/10" title="مشاهده در دیوار"> |
|
|
|
<ExternalLink className="w-5 h-5" /> |
|
|
|
</a> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -566,51 +562,51 @@ function App() { |
|
|
|
|
|
|
|
{/* TAB 3: Logs View */} |
|
|
|
{activeTab === 'logs' && ( |
|
|
|
<div className="tab-view active"> |
|
|
|
<div className="animate-fade-in"> |
|
|
|
{loadingLogs ? ( |
|
|
|
<div className="loading-state"> |
|
|
|
<div className="spinner"></div> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<div className="border-3 border-white/5 border-t-purple-500 rounded-full w-9 h-9 animate-spin mb-4"></div> |
|
|
|
<p>در حال دریافت تاریخچه اجرا...</p> |
|
|
|
</div> |
|
|
|
) : logs.length === 0 ? ( |
|
|
|
<div className="empty-state"> |
|
|
|
<AlertTriangle /> |
|
|
|
<div className="flex flex-col items-center justify-center py-16 px-8 text-center w-full bg-slate-900/40 border border-dashed border-white/5 rounded-2xl text-gray-400"> |
|
|
|
<AlertTriangle className="w-12 h-12 mb-4 text-gray-500" /> |
|
|
|
<p>هیچ لاگ تاریخی ثبت نشده است.</p> |
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
<div className="table-container"> |
|
|
|
<table className="data-table"> |
|
|
|
<thead> |
|
|
|
<div className="bg-slate-900/40 backdrop-blur-md border border-white/5 rounded-2xl shadow-xl overflow-hidden w-full overflow-x-auto"> |
|
|
|
<table className="min-w-full text-right text-sm text-gray-300 border-collapse"> |
|
|
|
<thead className="bg-black/20 text-white font-bold"> |
|
|
|
<tr> |
|
|
|
<th>کرالر</th> |
|
|
|
<th>شروع اجرا</th> |
|
|
|
<th>پایان اجرا</th> |
|
|
|
<th>وضعیت</th> |
|
|
|
<th>آگهی یافت شده</th> |
|
|
|
<th>پردازش AI</th> |
|
|
|
<th>پرچمگذاری شده</th> |
|
|
|
<th>لاگ خطا</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">کرالر</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">شروع اجرا</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">پایان اجرا</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">وضعیت</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">آگهی یافت شده</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">پردازش AI</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">پرچمگذاری شده</th> |
|
|
|
<th className="px-5 py-4 border-b border-white/5">لاگ خطا</th> |
|
|
|
</tr> |
|
|
|
</thead> |
|
|
|
<tbody> |
|
|
|
{logs.map(run => ( |
|
|
|
<tr key={run.id}> |
|
|
|
<td style={{ fontWeight: 600 }}>{run.crawler_title}</td> |
|
|
|
<td className="arabic-num-font">{formatDate(run.started_at)}</td> |
|
|
|
<td className="arabic-num-font">{formatDate(run.finished_at)}</td> |
|
|
|
<td> |
|
|
|
<span className={`badge-status ${run.status.toLowerCase()}`}> |
|
|
|
<tr key={run.id} className="hover:bg-white/1"> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 font-semibold">{run.crawler_title}</td> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 font-en text-xs">{formatDate(run.started_at)}</td> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 font-en text-xs">{formatDate(run.finished_at)}</td> |
|
|
|
<td className="px-5 py-4 border-b border-white/5"> |
|
|
|
<span className={`px-2.5 py-1 rounded-full text-xs font-medium inline-flex items-center gap-1 border ${run.status === 'SUCCESS' ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30' : run.status === 'FAILED' ? 'bg-rose-500/10 text-rose-400 border-rose-500/30' : 'bg-amber-500/10 text-amber-400 border-amber-500/30'}`}> |
|
|
|
{run.status === 'RUNNING' && 'در حال اجرا'} |
|
|
|
{run.status === 'SUCCESS' && 'موفق'} |
|
|
|
{run.status === 'FAILED' && 'ناموفق'} |
|
|
|
</span> |
|
|
|
</td> |
|
|
|
<td className="arabic-num-font">{run.ads_fetched_count}</td> |
|
|
|
<td className="arabic-num-font">{run.ads_evaluated_count}</td> |
|
|
|
<td className="arabic-num-font" style={{ color: run.ads_flagged_count > 0 ? 'var(--accent)' : 'inherit', fontWeight: run.ads_flagged_count > 0 ? 700 : 'normal' }}> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 font-en">{run.ads_fetched_count}</td> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 font-en">{run.ads_evaluated_count}</td> |
|
|
|
<td className={`px-5 py-4 border-b border-white/5 font-en ${run.ads_flagged_count > 0 ? 'text-purple-400 font-bold' : ''}`}> |
|
|
|
{run.ads_flagged_count} |
|
|
|
</td> |
|
|
|
<td style={{ maxWidth: '180px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={run.error_log}> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 max-w-[180px] overflow-hidden text-ellipsis whitespace-nowrap" title={run.error_log}> |
|
|
|
{run.error_log || '-'} |
|
|
|
</td> |
|
|
|
</tr> |
|
|
|
@ -626,67 +622,67 @@ function App() { |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* Form Dialog Modal */} |
|
|
|
<div className={`modal-overlay ${modalOpen ? 'open' : ''}`}> |
|
|
|
<div className="modal-card"> |
|
|
|
<div className="modal-header"> |
|
|
|
<h3>{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}</h3> |
|
|
|
<button className="close-btn" onClick={closeCrawlerModal}> |
|
|
|
<X /> |
|
|
|
<div className={`fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 transition-opacity duration-200 ${modalOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}> |
|
|
|
<div className={`bg-slate-900 border border-white/5 rounded-2xl w-[600px] max-w-[95%] max-h-[90vh] shadow-[0_20px_50px_rgba(0,0,0,0.6)] flex flex-col transition-transform duration-200 ${modalOpen ? 'scale-100' : 'scale-95'}`}> |
|
|
|
<div className="flex items-center justify-between p-5 border-b border-white/5"> |
|
|
|
<h3 className="text-base font-bold text-white">{editingCrawler ? 'ویرایش کرالر' : 'تعریف کرالر جدید'}</h3> |
|
|
|
<button className="bg-transparent border-none text-gray-400 cursor-pointer flex items-center hover:text-white" onClick={closeCrawlerModal}> |
|
|
|
<X className="w-5 h-5" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<form onSubmit={handleFormSubmit}> |
|
|
|
<div className="modal-body"> |
|
|
|
<form onSubmit={handleFormSubmit} className="flex flex-col min-h-0"> |
|
|
|
<div className="p-6 overflow-y-auto flex flex-col gap-5"> |
|
|
|
{formError && ( |
|
|
|
<div style={{ |
|
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)', |
|
|
|
border: '1px solid var(--danger)', |
|
|
|
padding: '10px', |
|
|
|
borderRadius: '6px', |
|
|
|
fontSize: '0.85rem', |
|
|
|
color: '#fca5a5' |
|
|
|
}}> |
|
|
|
<div className="bg-rose-500/10 border border-rose-500/30 p-3 rounded-lg text-xs text-rose-300"> |
|
|
|
{formError} |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
<div className="form-group"> |
|
|
|
<label>عنوان کرالر</label> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">عنوان کرالر</label> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
value={formTitle} |
|
|
|
onChange={(e) => setFormTitle(e.target.value)} |
|
|
|
placeholder="مثال: رهن کامل فوری نیاوران" |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]" |
|
|
|
required |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-group"> |
|
|
|
<label>لینک فیلتر دیوار (باید با https://divar.ir/s/ شروع شود)</label> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">لینک فیلتر دیوار (باید با https://divar.ir/s/ شروع شود)</label> |
|
|
|
<input |
|
|
|
type="url" |
|
|
|
value={formUrl} |
|
|
|
onChange={(e) => setFormUrl(e.target.value)} |
|
|
|
placeholder="https://divar.ir/s/tehran/rent-apartment/niavaran?..." |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]" |
|
|
|
required |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-group"> |
|
|
|
<label>پرامپت ارزیابی هوش مصنوعی (AI)</label> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">پرامپت ارزیابی هوش مصنوعی (AI)</label> |
|
|
|
<textarea |
|
|
|
value={formPrompt} |
|
|
|
onChange={(e) => setFormPrompt(e.target.value)} |
|
|
|
placeholder="بررسی کن آیا آگهی پول لازم و رهن کامل است یا خیر..." |
|
|
|
rows={3} |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)] resize-vertical" |
|
|
|
required |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-row"> |
|
|
|
<div className="form-group"> |
|
|
|
<label>بازه زمانی پایش</label> |
|
|
|
<select value={formInterval} onChange={(e) => setFormInterval(e.target.value)}> |
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">بازه زمانی پایش</label> |
|
|
|
<select |
|
|
|
value={formInterval} |
|
|
|
onChange={(e) => setFormInterval(e.target.value)} |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500" |
|
|
|
> |
|
|
|
<option value="5">۵ دقیقه</option> |
|
|
|
<option value="15">۱۵ دقیقه</option> |
|
|
|
<option value="30">۳۰ دقیقه</option> |
|
|
|
@ -699,56 +695,60 @@ function App() { |
|
|
|
</select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-group"> |
|
|
|
<label>آیدی کانال تلگرام (اختیاری)</label> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">آیدی کانال تلگرام (اختیاری)</label> |
|
|
|
<input |
|
|
|
type="text" |
|
|
|
value={formTelegram} |
|
|
|
onChange={(e) => setFormTelegram(e.target.value)} |
|
|
|
placeholder="مثال: @my_alerts" |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500 focus:shadow-[0_0_10px_rgba(168,85,247,0.2)]" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-row"> |
|
|
|
<div className="form-group"> |
|
|
|
<label>ساعت شروع پنجره مجاز</label> |
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">ساعت شروع پنجره مجاز</label> |
|
|
|
<input |
|
|
|
type="time" |
|
|
|
value={formStartHour} |
|
|
|
onChange={(e) => setFormStartHour(e.target.value)} |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500" |
|
|
|
required |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-group"> |
|
|
|
<label>ساعت پایان پنجره مجاز</label> |
|
|
|
<div className="flex flex-col gap-1.5"> |
|
|
|
<label className="text-xs text-gray-400 font-medium">ساعت پایان پنجره مجاز</label> |
|
|
|
<input |
|
|
|
type="time" |
|
|
|
value={formEndHour} |
|
|
|
onChange={(e) => setFormEndHour(e.target.value)} |
|
|
|
className="bg-black/20 border border-white/5 rounded-lg px-4 py-2 text-white text-sm outline-none focus:border-purple-500" |
|
|
|
required |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="form-group toggle-group"> |
|
|
|
<span className="toggle-label" style={{ fontSize: '0.85rem' }}>کرالر فعال باشد</span> |
|
|
|
<label className="switch-container"> |
|
|
|
<div className="flex flex-row justify-between items-center bg-white/1 p-3.5 rounded-lg border border-white/5"> |
|
|
|
<span className="text-xs text-gray-300 font-medium">کرالر فعال باشد</span> |
|
|
|
<label className="relative inline-flex items-center cursor-pointer"> |
|
|
|
<input |
|
|
|
type="checkbox" |
|
|
|
checked={formIsActive} |
|
|
|
onChange={(e) => setFormIsActive(e.target.checked)} |
|
|
|
className="sr-only peer" |
|
|
|
/> |
|
|
|
<span class="switch-slider"></span> |
|
|
|
<div className="w-11 h-6 bg-white/10 border border-white/10 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-gray-400 after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600 peer-checked:after:bg-white"></div> |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="modal-footer"> |
|
|
|
<button type="button" className="btn btn-secondary" onClick={closeCrawlerModal}>انصراف</button> |
|
|
|
<button type="submit" className="btn btn-primary">ذخیره</button> |
|
|
|
<div className="flex justify-end gap-2 p-5 border-t border-white/5"> |
|
|
|
<button type="button" className="inline-flex items-center justify-center border border-white/5 rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 text-white hover:bg-white/10" onClick={closeCrawlerModal}>انصراف</button> |
|
|
|
<button type="submit" className="inline-flex items-center justify-center border border-transparent rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-purple-500 text-white shadow-[0_4px_14px_rgba(168,85,247,0.3)] hover:bg-purple-600 hover:-translate-y-0.5">ذخیره</button> |
|
|
|
</div> |
|
|
|
</form> |
|
|
|
</div> |
|
|
|
|