feat: implement QuestionBank CRUD with pagination and template query

- Add pagination support to findAll (page, limit query params)
- Add findByTemplateId method to service
- Add GET /by-template/:templateId endpoint to controller
- Service already includes CRUD for QuestionBank and QuestionBankItem
This commit is contained in:
Developer
2026-04-23 17:19:11 +08:00
commit 0a9588abb7
492 changed files with 112453 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
/**
* 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 jsx>{`
.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>
);
};