taghirsado / components /History.tsx
Opera10's picture
Upload 10 files
a8df197 verified
Raw
History Blame Contribute Delete
7.43 kB
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;