Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { JobData } from '../types'; | |
| import ProcessingPanel from './ProcessingPanel'; | |
| import { MODELS } from '../constants'; | |
| interface HistoryProps { | |
| jobs: JobData[]; | |
| onClearAll: () => void; | |
| onDeleteSingle: (jobId: string) => void; | |
| onSelect: (job: JobData) => void; | |
| onEnhance: (job: JobData) => void; | |
| } | |
| const History: React.FC<HistoryProps> = ({ jobs, onClearAll, onDeleteSingle, onSelect, onEnhance }) => { | |
| // Sort jobs by timestamp (newest first) | |
| const sortedJobs = [...jobs].sort((a, b) => { | |
| const timeA = a.timestamp || 0; | |
| const timeB = b.timestamp || 0; | |
| return timeB - timeA; | |
| }); | |
| const [expandedId, setExpandedId] = useState<string | null>(null); | |
| const toggleExpand = (job: JobData) => { | |
| if (expandedId === job.job_id) { | |
| setExpandedId(null); | |
| } else { | |
| setExpandedId(job.job_id); | |
| onSelect(job); | |
| } | |
| }; | |
| const handleDeleteClick = (e: React.MouseEvent, jobId: string) => { | |
| e.stopPropagation(); | |
| onDeleteSingle(jobId); | |
| }; | |
| const getModelImageUrl = (job: JobData) => { | |
| // Priority 1: Check if modelImage is explicitly provided (especially for custom models) | |
| if (job.modelImage) { | |
| if (typeof job.modelImage === 'string') return job.modelImage; | |
| try { | |
| // If it's a File or Blob, create a preview URL | |
| return URL.createObjectURL(job.modelImage as any); | |
| } catch (e) { | |
| return null; | |
| } | |
| } | |
| // Priority 2: Look up by name for standard models | |
| if (job.type === 'model' && job.modelName) { | |
| const found = MODELS.find(m => m.name === job.modelName); | |
| if (found) return found.image as string; | |
| } | |
| return null; | |
| }; | |
| return ( | |
| <div className="animate-[fadeIn_0.5s_ease-out]"> | |
| <div className="flex items-center justify-between mb-4 px-3 mt-2"> | |
| <div> | |
| <h2 className="text-xl font-black text-gray-800">سوابق پروژه</h2> | |
| <p className="text-xs text-gray-500 mt-1">لیست درخواستهای پردازش شده</p> | |
| </div> | |
| {jobs.length > 0 && ( | |
| <button | |
| onClick={onClearAll} | |
| className="px-3 py-1.5 rounded-full bg-red-50 text-red-500 hover:bg-red-100 flex items-center gap-2 transition-colors shadow-sm text-xs font-bold" | |
| > | |
| <i className="fas fa-trash-alt"></i> حذف همه | |
| </button> | |
| )} | |
| </div> | |
| <div className="space-y-3 pb-24 px-1"> | |
| {jobs.length === 0 ? ( | |
| <div className="text-center py-24 opacity-50"> | |
| <div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4 grayscale"> | |
| <i className="fas fa-folder-open text-3xl text-gray-300"></i> | |
| </div> | |
| <p className="text-gray-400 font-medium text-sm">هنوز هیچ پروژهای انجام نشده است</p> | |
| </div> | |
| ) : ( | |
| sortedJobs.map((job) => { | |
| const isExpanded = expandedId === job.job_id; | |
| const modelImage = getModelImageUrl(job); | |
| return ( | |
| <div | |
| key={job.job_id} | |
| onClick={() => toggleExpand(job)} | |
| className={`group relative bg-white rounded-2xl px-4 py-4 border transition-all duration-300 cursor-pointer overflow-hidden | |
| ${isExpanded | |
| ? 'border-primary shadow-lg ring-1 ring-primary/10' | |
| : 'border-gray-100 shadow-sm hover:shadow-md'}`} | |
| > | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-4 overflow-hidden"> | |
| <div className={`w-12 h-12 flex-shrink-0 rounded-full flex items-center justify-center shadow-md transition-all duration-300 border-2 border-white overflow-hidden | |
| ${modelImage ? 'bg-gray-200' : (job.type === 'model' ? 'bg-gradient-to-br from-indigo-500 to-purple-600' : 'bg-gradient-to-br from-gray-700 to-gray-900')}`}> | |
| {modelImage ? ( | |
| <img | |
| src={modelImage} | |
| alt="Model" | |
| className="w-full h-full object-cover" | |
| onLoad={(e) => { | |
| // Basic URL management if it's a blob | |
| if (modelImage.startsWith('blob:')) { | |
| // URL.revokeObjectURL(modelImage); // Revoke only when component unmounts for safety | |
| } | |
| }} | |
| /> | |
| ) : ( | |
| <div className="text-white"> | |
| {job.type === 'model' ? <i className="fas fa-user text-base"></i> : <i className="fas fa-fingerprint text-base"></i>} | |
| </div> | |
| )} | |
| </div> | |
| <div className="min-w-0"> | |
| <h3 className="font-bold text-gray-800 text-sm truncate leading-tight"> | |
| {job.type === 'model' ? job.modelName : 'پروژه اختصاصی'} | |
| </h3> | |
| <div className="flex items-center gap-2 mt-1.5"> | |
| <span className={`text-[10px] px-2 py-0.5 rounded-md font-bold border | |
| ${job.status === 'completed' ? 'bg-green-50 text-green-600 border-green-100' | |
| : job.status === 'failed' ? 'bg-red-50 text-red-600 border-red-100' | |
| : 'bg-amber-50 text-amber-600 border-amber-100'}`}> | |
| {job.status === 'completed' ? 'تکمیل شده' : job.status === 'failed' ? 'خطا' : 'در حال کار'} | |
| </span> | |
| <span className="text-[10px] text-gray-400 dir-ltr font-medium">{job.date?.split(',')[0]}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <i className={`fas fa-chevron-left text-gray-300 text-sm transition-transform duration-300 ${isExpanded ? '-rotate-90 text-primary' : ''}`}></i> | |
| <button | |
| onClick={(e) => handleDeleteClick(e, job.job_id)} | |
| className="w-8 h-8 flex items-center justify-center rounded-full text-gray-300 bg-gray-50 hover:text-red-500 hover:bg-red-50 transition-colors z-10 text-sm border border-transparent hover:border-red-100" | |
| > | |
| <i className="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div className={`grid transition-[grid-template-rows] duration-300 ease-out ${isExpanded ? 'grid-rows-[1fr] opacity-100 mt-2' : 'grid-rows-[0fr] opacity-0 mt-0'}`}> | |
| <div className="overflow-hidden"> | |
| <ProcessingPanel | |
| job={job} | |
| embedded={true} | |
| onEnhance={onEnhance} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default History; | |