This commit is contained in:
egg
2025-12-04 18:00:37 +08:00
parent 9437387ef1
commit 8265be1741
22 changed files with 2672 additions and 196 deletions

View File

@@ -0,0 +1,148 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Cpu, FileText, Sparkles, Info } from 'lucide-react'
import type { ProcessingTrack, DocumentAnalysisResponse } from '@/types/apiV2'
interface ProcessingTrackSelectorProps {
value: ProcessingTrack | null // null means "use system recommendation"
onChange: (track: ProcessingTrack | null) => void
documentAnalysis?: DocumentAnalysisResponse | null
disabled?: boolean
}
export default function ProcessingTrackSelector({
value,
onChange,
documentAnalysis,
disabled = false,
}: ProcessingTrackSelectorProps) {
const recommendedTrack = documentAnalysis?.recommended_track
const tracks = [
{
id: null as ProcessingTrack | null,
name: '自動選擇',
description: '根據文件類型自動選擇最佳處理方式',
icon: Sparkles,
color: 'text-purple-600',
bgColor: 'bg-purple-50',
borderColor: 'border-purple-200',
recommended: false,
},
{
id: 'direct' as ProcessingTrack,
name: '直接提取 (DIRECT)',
description: '從 PDF 中直接提取文字圖層,適用於可編輯 PDF',
icon: FileText,
color: 'text-blue-600',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
recommended: recommendedTrack === 'direct',
},
{
id: 'ocr' as ProcessingTrack,
name: 'OCR 識別',
description: '使用光學字元識別處理圖片或掃描文件',
icon: Cpu,
color: 'text-green-600',
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
recommended: recommendedTrack === 'ocr',
},
]
return (
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg">
<Sparkles className="w-5 h-5 text-primary" />
</div>
<div>
<CardTitle></CardTitle>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Info about override */}
{value !== null && recommendedTrack && value !== recommendedTrack && (
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<Info className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-800">
使{recommendedTrack === 'direct' ? '直接提取' : 'OCR 識別'}
</p>
</div>
)}
{/* Track options */}
<div className="grid gap-3">
{tracks.map((track) => {
const isSelected = value === track.id
const Icon = track.icon
return (
<button
key={track.id ?? 'auto'}
type="button"
disabled={disabled}
onClick={() => onChange(track.id)}
className={`
w-full p-4 rounded-lg border-2 text-left transition-all
${isSelected
? `${track.borderColor} ${track.bgColor}`
: 'border-border hover:border-primary/30 hover:bg-muted/30'
}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
>
<div className="flex items-start gap-3">
<div className={`p-2 rounded-lg ${isSelected ? track.bgColor : 'bg-muted'}`}>
<Icon className={`w-5 h-5 ${isSelected ? track.color : 'text-muted-foreground'}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className={`font-medium ${isSelected ? track.color : ''}`}>
{track.name}
</span>
{track.recommended && (
<Badge variant="outline" className="text-xs bg-white">
</Badge>
)}
{isSelected && (
<Badge variant="default" className="text-xs">
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">
{track.description}
</p>
</div>
</div>
</button>
)
})}
</div>
{/* Current analysis info */}
{documentAnalysis && (
<div className="pt-3 border-t border-border">
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
<span>: {(documentAnalysis.confidence * 100).toFixed(0)}%</span>
{documentAnalysis.page_count && (
<span>: {documentAnalysis.page_count}</span>
)}
{documentAnalysis.text_coverage !== null && (
<span>: {(documentAnalysis.text_coverage * 100).toFixed(1)}%</span>
)}
</div>
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -8,14 +8,15 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/toast'
import { apiClientV2 } from '@/services/apiV2'
import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2, Info } from 'lucide-react'
import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react'
import LayoutModelSelector from '@/components/LayoutModelSelector'
import PreprocessingSettings from '@/components/PreprocessingSettings'
import PreprocessingPreview from '@/components/PreprocessingPreview'
import TableDetectionSelector from '@/components/TableDetectionSelector'
import ProcessingTrackSelector from '@/components/ProcessingTrackSelector'
import TaskNotFound from '@/components/TaskNotFound'
import { useTaskValidation } from '@/hooks/useTaskValidation'
import type { LayoutModel, ProcessingOptions, PreprocessingMode, PreprocessingConfig, TableDetectionConfig, DocumentAnalysisResponse } from '@/types/apiV2'
import type { LayoutModel, ProcessingOptions, PreprocessingMode, PreprocessingConfig, TableDetectionConfig, ProcessingTrack } from '@/types/apiV2'
export default function ProcessingPage() {
const { t } = useTranslation()
@@ -56,6 +57,9 @@ export default function ProcessingPage() {
enable_region_detection: true,
})
// Processing track override state (null = use system recommendation)
const [forceTrack, setForceTrack] = useState<ProcessingTrack | null>(null)
// Analyze document to determine if OCR is needed (only for pending tasks)
const { data: documentAnalysis, isLoading: isAnalyzing } = useQuery({
queryKey: ['documentAnalysis', taskId],
@@ -65,16 +69,23 @@ export default function ProcessingPage() {
})
// Determine if preprocessing options should be shown
// Only show for OCR track files (images and non-editable PDFs)
const needsOcrTrack = documentAnalysis?.recommended_track === 'ocr' ||
documentAnalysis?.recommended_track === 'hybrid' ||
!documentAnalysis // Show by default while analyzing
// Show OCR options when:
// 1. User explicitly selected OCR track
// 2. OR system recommends OCR/hybrid track (and user hasn't overridden to direct)
// 3. OR still analyzing (show by default)
const needsOcrTrack = forceTrack === 'ocr' ||
(forceTrack === null && (
documentAnalysis?.recommended_track === 'ocr' ||
documentAnalysis?.recommended_track === 'hybrid' ||
!documentAnalysis
))
// Start OCR processing
const processOCRMutation = useMutation({
mutationFn: () => {
const options: ProcessingOptions = {
use_dual_track: true,
use_dual_track: forceTrack === null, // Only use dual-track auto-detection if not forcing
force_track: forceTrack || undefined, // Pass force_track if user selected one
language: 'ch',
layout_model: layoutModel,
preprocessing_mode: preprocessingMode,
@@ -392,53 +403,14 @@ export default function ProcessingPage() {
</div>
)}
{/* Document Analysis Info */}
{documentAnalysis && (
<Card className={documentAnalysis.recommended_track === 'direct' ? 'border-blue-200 bg-blue-50' : 'border-green-200 bg-green-50'}>
<CardContent className="pt-4">
<div className="flex items-start gap-3">
<Info className={`w-5 h-5 flex-shrink-0 mt-0.5 ${documentAnalysis.recommended_track === 'direct' ? 'text-blue-600' : 'text-green-600'}`} />
<div className="flex-1">
{documentAnalysis.recommended_track === 'direct' ? (
<>
<p className="text-sm font-medium text-blue-800"> PDF</p>
<p className="text-sm text-blue-700 mt-1">
PDF 使
</p>
</>
) : (
<>
<p className="text-sm font-medium text-green-800">
{documentAnalysis.is_editable ? '混合文件' : '掃描文件 / 影像'}
</p>
<p className="text-sm text-green-700 mt-1">
{documentAnalysis.reason}
</p>
</>
)}
<div className="flex flex-wrap gap-4 mt-2 text-xs">
<span className={documentAnalysis.recommended_track === 'direct' ? 'text-blue-600' : 'text-green-600'}>
: {documentAnalysis.recommended_track === 'direct' ? '直接提取' : documentAnalysis.recommended_track === 'ocr' ? 'OCR 識別' : '混合處理'}
</span>
{documentAnalysis.page_count && (
<span className={documentAnalysis.recommended_track === 'direct' ? 'text-blue-600' : 'text-green-600'}>
: {documentAnalysis.page_count}
</span>
)}
{documentAnalysis.text_coverage !== null && (
<span className={documentAnalysis.recommended_track === 'direct' ? 'text-blue-600' : 'text-green-600'}>
: {(documentAnalysis.text_coverage * 100).toFixed(1)}%
</span>
)}
<span className={documentAnalysis.recommended_track === 'direct' ? 'text-blue-600' : 'text-green-600'}>
: {(documentAnalysis.confidence * 100).toFixed(0)}%
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Processing Track Selector - Always show after analysis */}
{!isAnalyzing && (
<ProcessingTrackSelector
value={forceTrack}
onChange={setForceTrack}
documentAnalysis={documentAnalysis}
disabled={processOCRMutation.isPending}
/>
)}
{/* OCR Track Options - Only show when document needs OCR */}