- 证书API 500修复: AssessmentCertificate实体注册到app.module.ts - 前端TS错误25个清零: i18n key 17个, 类型定义8个 - i18n补全: 17个缺失key添加到zh/en/ja - KnowledgeFile类型: 添加title, content字段 - importService: 改用apiClient.request替代raw fetch - ModeSelector: 移除jsx prop - questionBankService: .ok -> .status >= 400 - NotebookDetailView: .filter -> .items.filter - ImportTasksDrawer: tasks.items提取 - API端点审计: 16/16通过 - 数据库Schema审计: 25表288列一致 - AGENTS.md更新
250 lines
6.3 KiB
TypeScript
250 lines
6.3 KiB
TypeScript
/**
|
|
* Processing mode selection component
|
|
* Used to select fast or precise mode when uploading files
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { uploadService } from '../services/uploadService';
|
|
import { ModeRecommendation } from '../types';
|
|
|
|
interface ModeSelectorProps {
|
|
file: File | null;
|
|
onModeChange: (mode: 'fast' | 'precise') => void;
|
|
className?: string;
|
|
}
|
|
|
|
export const ModeSelector: React.FC<ModeSelectorProps> = ({
|
|
file,
|
|
onModeChange,
|
|
className = '',
|
|
}) => {
|
|
const [selectedMode, setSelectedMode] = useState<'fast' | 'precise'>('fast');
|
|
const [recommendation, setRecommendation] = useState<ModeRecommendation | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (file) {
|
|
loadRecommendation();
|
|
} else {
|
|
setRecommendation(null);
|
|
}
|
|
}, [file]);
|
|
|
|
const loadRecommendation = async () => {
|
|
if (!file) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const rec = await uploadService.recommendMode(file);
|
|
setRecommendation(rec);
|
|
|
|
// Automatically select recommended mode
|
|
setSelectedMode(rec.recommendedMode);
|
|
onModeChange(rec.recommendedMode);
|
|
} catch (error) {
|
|
console.error('Failed to get mode recommendation:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleModeChange = (mode: 'fast' | 'precise') => {
|
|
setSelectedMode(mode);
|
|
onModeChange(mode);
|
|
};
|
|
|
|
if (!file) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className={`mode-selector ${className}`}>
|
|
<div className="mode-selector-header">
|
|
<h4>Select processing mode</h4>
|
|
{loading && <span className="loading">Analyzing...</span>}
|
|
</div>
|
|
|
|
{/* Mode recommendation info */}
|
|
{recommendation && (
|
|
<div className="recommendation-info">
|
|
<div className="reason">
|
|
<strong>Recommended:</strong> {recommendation.reason}
|
|
</div>
|
|
|
|
{recommendation.warnings && recommendation.warnings.length > 0 && (
|
|
<div className="warnings">
|
|
{recommendation.warnings.map((warning, idx) => (
|
|
<div key={idx} className="warning-item">
|
|
⚠️ {warning}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Mode selection */}
|
|
<div className="mode-options">
|
|
<label className={`mode-option ${selectedMode === 'fast' ? 'selected' : ''}`}>
|
|
<input
|
|
type="radio"
|
|
name="processing-mode"
|
|
value="fast"
|
|
checked={selectedMode === 'fast'}
|
|
onChange={() => handleModeChange('fast')}
|
|
/>
|
|
<div className="mode-content">
|
|
<div className="mode-title">⚡ Fast Mode</div>
|
|
<div className="mode-desc">
|
|
Simple text extraction, fast, ideal for plain text documents
|
|
</div>
|
|
<div className="mode-benefits">
|
|
✅ Fast<br />
|
|
✅ No additional cost<br />
|
|
❌ Processes text information only
|
|
</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label className={`mode-option ${selectedMode === 'precise' ? 'selected' : ''}`}>
|
|
<input
|
|
type="radio"
|
|
name="processing-mode"
|
|
value="precise"
|
|
checked={selectedMode === 'precise'}
|
|
onChange={() => handleModeChange('precise')}
|
|
/>
|
|
<div className="mode-content">
|
|
<div className="mode-title">🎯 Precise Mode</div>
|
|
<div className="mode-desc">
|
|
Accurately recognizes content and retains full information
|
|
</div>
|
|
<div className="mode-benefits">
|
|
✅ Recognizes images/tables<br />
|
|
✅ Retains layout information<br />
|
|
✅ Mixed image and text content<br />
|
|
⚠️ API cost required<br />
|
|
⚠️ Long processing time
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<style>{`
|
|
.mode-selector {
|
|
margin: 16px 0;
|
|
padding: 16px;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.mode-selector-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.mode-selector-header h4 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.loading {
|
|
color: #1890ff;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.recommendation-info {
|
|
margin-bottom: 16px;
|
|
padding: 12px;
|
|
background: #e6f7ff;
|
|
border: 1px solid #91d5ff;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.recommendation-info .reason {
|
|
margin-bottom: 8px;
|
|
color: #0050b3;
|
|
}
|
|
|
|
.warnings {
|
|
margin-top: 8px;
|
|
padding-top: 8px;
|
|
border-top: 1px dashed #91d5ff;
|
|
}
|
|
|
|
.warning-item {
|
|
color: #d4380d;
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.mode-options {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.mode-option {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
border: 2px solid #d9d9d9;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
}
|
|
|
|
.mode-option:hover {
|
|
border-color: #1890ff;
|
|
background: #f0f7ff;
|
|
}
|
|
|
|
.mode-option.selected {
|
|
border-color: #1890ff;
|
|
background: #e6f7ff;
|
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
|
}
|
|
|
|
.mode-option input[type="radio"] {
|
|
margin-top: 4px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mode-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.mode-title {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.mode-desc {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.mode-benefits {
|
|
font-size: 11px;
|
|
line-height: 1.6;
|
|
color: #555;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.mode-options {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|