Files
OCR/frontend/src/components/LayoutModelSelector.tsx
egg 59206a6ab8 feat: simplify layout model selection and archive proposals
Changes:
- Replace PP-Structure 7-slider parameter UI with simple 3-option layout model selector
- Add layout model mapping: chinese (PP-DocLayout-S), default (PubLayNet), cdla
- Add LayoutModelSelector component and zh-TW translations
- Fix "default" model behavior with sentinel value for PubLayNet
- Add gap filling service for OCR track coverage improvement
- Add PP-Structure debug utilities
- Archive completed/incomplete proposals:
  - add-ocr-track-gap-filling (complete)
  - fix-ocr-track-table-rendering (incomplete)
  - simplify-ppstructure-model-selection (22/25 tasks)
- Add new layout model tests, archive old PP-Structure param tests
- Update OpenSpec ocr-processing spec with layout model requirements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 13:27:00 +08:00

111 lines
3.5 KiB
TypeScript

import { cn } from '@/lib/utils'
import { Check, FileText, Globe, BookOpen } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { LayoutModel } from '@/types/apiV2'
interface LayoutModelSelectorProps {
value: LayoutModel
onChange: (model: LayoutModel) => void
disabled?: boolean
className?: string
}
const MODEL_ICONS: Record<LayoutModel, React.ReactNode> = {
chinese: <FileText className="w-5 h-5" />,
default: <Globe className="w-5 h-5" />,
cdla: <BookOpen className="w-5 h-5" />,
}
export default function LayoutModelSelector({
value,
onChange,
disabled = false,
className,
}: LayoutModelSelectorProps) {
const { t } = useTranslation()
const models: LayoutModel[] = ['chinese', 'default', 'cdla']
const getModelInfo = (model: LayoutModel) => ({
label: t(`processing.layoutModel.${model}`),
description: t(`processing.layoutModel.${model}Desc`),
})
return (
<div className={cn('border rounded-lg p-4 bg-white', className)}>
{/* Header */}
<div className="flex items-center gap-2 mb-4">
<FileText className="w-5 h-5 text-gray-600" />
<h3 className="text-lg font-semibold text-gray-900">{t('processing.layoutModel.title')}</h3>
</div>
{/* Model Options */}
<div className="space-y-3">
{models.map((model) => {
const info = getModelInfo(model)
const isSelected = value === model
return (
<button
key={model}
type="button"
disabled={disabled}
onClick={() => onChange(model)}
className={cn(
'w-full flex items-start gap-4 p-4 rounded-lg border-2 transition-all text-left',
isSelected
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50',
disabled && 'opacity-50 cursor-not-allowed'
)}
>
{/* Icon */}
<div
className={cn(
'p-2 rounded-lg flex-shrink-0',
isSelected ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500'
)}
>
{MODEL_ICONS[model]}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span
className={cn(
'font-medium',
isSelected ? 'text-blue-700' : 'text-gray-900'
)}
>
{info.label}
</span>
{model === 'chinese' && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
{t('processing.layoutModel.recommended')}
</span>
)}
</div>
<p className="text-sm text-gray-500 mt-1">{info.description}</p>
</div>
{/* Check mark */}
{isSelected && (
<div className="flex-shrink-0">
<Check className="w-5 h-5 text-blue-600" />
</div>
)}
</button>
)
})}
</div>
{/* Info Note */}
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-800">
{t('processing.layoutModel.note')}
</p>
</div>
</div>
)
}