M3: console.log -> Logger + UI redesign (QuestionBank) + S7/A9/A10/A11/U11 bug fixes + #1/#2/#3/#4 enhancements + i18n for QuestionBank pages

This commit is contained in:
Developer
2026-05-19 16:57:45 +08:00
parent 5b5f14674d
commit 29bac74b58
20 changed files with 1081 additions and 501 deletions
+26 -15
View File
@@ -51,6 +51,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
const [templates, setTemplates] = useState<AssessmentTemplate[]>([]);
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null);
const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout;
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -137,7 +138,11 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
setState(histState);
setSession(histSession);
} catch (err: any) {
setError(err.message || 'Failed to load historical assessment');
if (histSession.status === 'IN_PROGRESS') {
setError(t('cannotResumeInProgress'));
} else {
setError(err.message || 'Failed to load historical assessment');
}
} finally {
setIsLoading(false);
setLoadingHistoryId(null);
@@ -184,7 +189,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
...prev,
...event.data,
messages: event.data.messages
? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => pm.content === m.content && pm.role === m.role))]
? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))]
: prevMessages,
feedbackHistory: event.data.feedbackHistory
? [...(prev.feedbackHistory || []), ...event.data.feedbackHistory.filter((fh: any) => !(prev.feedbackHistory || []).some((pfh: any) => pfh.content === fh.content))]
@@ -227,7 +232,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
};
const handleSubmitAnswer = async () => {
if (!session || !inputValue.trim() || isLoading) return;
if (!session || !inputValue.trim() || isLoading || isTimedOut) return;
const answer = inputValue.trim();
setInputValue('');
@@ -252,7 +257,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
if (!prev) return event.data;
const prevMessages = prev.messages || [];
const mergedMessages = event.data.messages
? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => pm.content === m.content && pm.role === m.role))]
? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))]
: prevMessages;
return {
@@ -428,7 +433,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
{/* Assessment History Sidebar */}
{history.length > 0 && (
<div className="w-80 flex-none bg-white p-6 overflow-y-auto hidden lg:flex flex-col border-l border-slate-200/60 shadow-[4px_0_24px_rgba(0,0,0,0.02)]">
<div className="w-80 flex-none bg-white p-6 overflow-y-auto flex flex-col border-l border-slate-200/60 shadow-[4px_0_24px_rgba(0,0,0,0.02)]">
<h3 className="text-sm font-black text-slate-900 mb-6 flex items-center gap-2 uppercase tracking-widest">
<History size={18} className="text-indigo-600" />
{t('recentAssessments')}
@@ -576,26 +581,32 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
</div>
<div className="p-6 bg-white border-t border-slate-200/60 shadow-[0_-4px_20px_-10px_rgba(0,0,0,0.05)]">
{isTimedOut && (
<div className="max-w-3xl mx-auto mb-3 px-4 py-2 bg-red-50 border border-red-200 text-red-700 text-sm font-bold rounded-xl text-center">
{t('timeLimitExceeded')}
</div>
)}
<div className="max-w-3xl mx-auto flex items-end gap-3">
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === 'Enter' && !e.shiftKey && !isTimedOut) {
e.preventDefault();
handleSubmitAnswer();
}
}}
placeholder={t('typeAnswerPlaceholder')}
className="flex-1 max-h-32 p-4 bg-slate-50 border-none rounded-2xl focus:bg-white focus:ring-2 focus:ring-indigo-500/20 text-sm font-medium resize-none transition-all placeholder:text-slate-400 outline-none shadow-inner"
placeholder={isTimedOut ? t('timeLimitExceeded') : t('typeAnswerPlaceholder')}
disabled={isTimedOut}
className="flex-1 max-h-32 p-4 bg-slate-50 border-none rounded-2xl focus:bg-white focus:ring-2 focus:ring-indigo-500/20 text-sm font-medium resize-none transition-all placeholder:text-slate-400 outline-none shadow-inner disabled:opacity-50 disabled:cursor-not-allowed"
rows={1}
/>
<button
onClick={handleSubmitAnswer}
disabled={!inputValue.trim() || isLoading}
disabled={!inputValue.trim() || isLoading || isTimedOut}
className={cn(
"w-14 h-14 flex items-center justify-center rounded-2xl transition-all shadow-lg",
!inputValue.trim() || isLoading
!inputValue.trim() || isLoading || isTimedOut
? "bg-slate-100 text-slate-400 shadow-none"
: "bg-indigo-600 text-white hover:bg-indigo-700 shadow-indigo-200 active:scale-95"
)}
@@ -607,7 +618,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
</div>
{/* Right: Feedback Panel */}
<div className="w-80 flex-none bg-white p-6 overflow-y-auto hidden lg:flex flex-col border-l border-slate-100">
<div className="w-80 flex-none bg-white p-6 overflow-y-auto flex flex-col border-l border-slate-100">
<h3 className="text-sm font-black text-slate-900 mb-6 flex items-center gap-2 uppercase tracking-widest">
<ClipboardCheck size={18} className="text-indigo-600" />
{t('liveFeedback')}
@@ -744,9 +755,9 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{t('status')}</span>
<span className={cn(
"text-2xl font-black uppercase tracking-tighter",
(state?.finalScore || 0) >= 6 ? "text-emerald-600" : "text-rose-600"
state?.passed ? "text-emerald-600" : "text-rose-600"
)}>
{(state?.finalScore || 0) >= 6 ? t('verified') : t('fail')}
{state?.passed ? t('verified') : t('fail')}
</span>
</div>
</div>
@@ -788,7 +799,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
a.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error('Failed to export PDF:', err);
setError(t('exportAssessmentFailed'));
}
}}
className="px-6 py-4 bg-white border-2 border-slate-100 text-slate-700 rounded-2xl font-bold hover:bg-slate-50 transition-all active:scale-[0.98]"
@@ -813,7 +824,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
a.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error('Failed to export Excel:', err);
setError(t('exportAssessmentFailed'));
}
}}
className="px-6 py-4 bg-white border-2 border-slate-100 text-slate-700 rounded-2xl font-bold hover:bg-slate-50 transition-all active:scale-[0.98]"