feat: 重構 review-design 為真實複習系統模擬器
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b9b007b4b5
commit
f042da5848
|
|
@ -1,272 +1,364 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Navigation } from '@/components/shared/Navigation'
|
import { Navigation } from '@/components/shared/Navigation'
|
||||||
import {
|
import { ReviewRunner } from '@/components/review/ReviewRunner'
|
||||||
FlipMemoryTest,
|
import { ProgressTracker } from '@/components/review/ProgressTracker'
|
||||||
VocabChoiceTest,
|
|
||||||
SentenceFillTest,
|
// Store imports
|
||||||
SentenceReorderTest,
|
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
|
||||||
VocabListeningTest,
|
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
|
||||||
SentenceListeningTest,
|
import { useTestResultStore } from '@/store/review/useTestResultStore'
|
||||||
SentenceSpeakingTest
|
import { useReviewDataStore } from '@/store/review/useReviewDataStore'
|
||||||
} from '@/components/review/review-tests'
|
|
||||||
import exampleData from './example-data.json'
|
import exampleData from './example-data.json'
|
||||||
|
|
||||||
export default function ReviewTestsPage() {
|
// 動態測試資料集配置
|
||||||
const [logs, setLogs] = useState<string[]>([])
|
const TEST_DATASETS = {
|
||||||
const [activeTab, setActiveTab] = useState('FlipMemoryTest')
|
empty: {
|
||||||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
id: 'empty',
|
||||||
|
name: '清空測試',
|
||||||
|
description: '測試空資料狀態和錯誤處理',
|
||||||
|
flashcards: []
|
||||||
|
},
|
||||||
|
sample: {
|
||||||
|
id: 'sample',
|
||||||
|
name: `快速測試 (前5張卡)`,
|
||||||
|
description: '使用前5張詞卡進行快速測試',
|
||||||
|
flashcards: exampleData.data.slice(0, 5).map(card => ({
|
||||||
|
...card,
|
||||||
|
cefr: card.difficultyLevel || 'A1',
|
||||||
|
hasExampleImage: false,
|
||||||
|
primaryImageUrl: undefined,
|
||||||
|
exampleImages: []
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
full: {
|
||||||
|
id: 'full',
|
||||||
|
name: `完整資料集 (${exampleData.data.length}張卡)`,
|
||||||
|
description: '使用 example-data.json 的完整真實資料',
|
||||||
|
flashcards: exampleData.data.map(card => ({
|
||||||
|
...card,
|
||||||
|
cefr: card.difficultyLevel || 'A1',
|
||||||
|
hasExampleImage: false,
|
||||||
|
primaryImageUrl: undefined,
|
||||||
|
exampleImages: []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 測驗組件清單
|
export default function ReviewDesignPage() {
|
||||||
const testComponents = [
|
const [currentDataset, setCurrentDataset] = useState<string>('')
|
||||||
{ id: 'FlipMemoryTest', name: '翻卡記憶測試', color: 'bg-blue-50' },
|
const [isSimulating, setIsSimulating] = useState(false)
|
||||||
{ id: 'VocabChoiceTest', name: '詞彙選擇測試', color: 'bg-green-50' },
|
const [debugLogs, setDebugLogs] = useState<string[]>([])
|
||||||
{ id: 'SentenceFillTest', name: '句子填空測試', color: 'bg-yellow-50' },
|
|
||||||
{ id: 'SentenceReorderTest', name: '句子重排測試', color: 'bg-purple-50' },
|
|
||||||
{ id: 'VocabListeningTest', name: '詞彙聽力測試', color: 'bg-red-50' },
|
|
||||||
{ id: 'SentenceListeningTest', name: '句子聽力測試', color: 'bg-indigo-50' },
|
|
||||||
{ id: 'SentenceSpeakingTest', name: '句子口說測試', color: 'bg-pink-50' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 添加日誌函數
|
// Store 狀態監控
|
||||||
const addLog = (message: string) => {
|
const sessionStore = useReviewSessionStore()
|
||||||
|
const queueStore = useTestQueueStore()
|
||||||
|
const resultStore = useTestResultStore()
|
||||||
|
const dataStore = useReviewDataStore()
|
||||||
|
|
||||||
|
// 重置所有 Store
|
||||||
|
const resetAllStores = useCallback(() => {
|
||||||
|
sessionStore.resetSession()
|
||||||
|
queueStore.resetQueue()
|
||||||
|
resultStore.resetScore()
|
||||||
|
dataStore.resetData()
|
||||||
|
|
||||||
|
addLog('🔄 所有 Store 已重置')
|
||||||
|
setIsSimulating(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 匯入測試資料集
|
||||||
|
const importDataset = useCallback(async (datasetId: string) => {
|
||||||
|
const dataset = TEST_DATASETS[datasetId as keyof typeof TEST_DATASETS]
|
||||||
|
if (!dataset) return
|
||||||
|
|
||||||
|
addLog(`📁 開始匯入資料集: ${dataset.name}`)
|
||||||
|
|
||||||
|
// 重置 Store
|
||||||
|
resetAllStores()
|
||||||
|
|
||||||
|
// 模擬真實的資料載入流程
|
||||||
|
dataStore.setLoadingCards(true)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500)) // 模擬網路延遲
|
||||||
|
|
||||||
|
// 載入資料到 ReviewDataStore
|
||||||
|
dataStore.setDueCards(dataset.flashcards)
|
||||||
|
dataStore.setLoadingCards(false)
|
||||||
|
|
||||||
|
// 觸發測試佇列初始化
|
||||||
|
queueStore.initializeTestQueue(dataset.flashcards, [])
|
||||||
|
|
||||||
|
// 設置第一張卡片
|
||||||
|
if (dataset.flashcards.length > 0) {
|
||||||
|
sessionStore.setCurrentCard(dataset.flashcards[0])
|
||||||
|
sessionStore.setMounted(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentDataset(datasetId)
|
||||||
|
addLog(`✅ 已匯入 ${dataset.flashcards.length} 張詞卡`)
|
||||||
|
addLog(`📋 產生 ${queueStore.testItems.length} 個測驗項目`)
|
||||||
|
}, [resetAllStores, dataStore, queueStore, sessionStore])
|
||||||
|
|
||||||
|
// 開始模擬
|
||||||
|
const startSimulation = useCallback(() => {
|
||||||
|
if (!currentDataset) {
|
||||||
|
addLog('❌ 請先匯入測試資料')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSimulating(true)
|
||||||
|
addLog('🎮 開始複習模擬')
|
||||||
|
}, [currentDataset])
|
||||||
|
|
||||||
|
// 新增日誌
|
||||||
|
const addLog = useCallback((message: string) => {
|
||||||
const timestamp = new Date().toLocaleTimeString()
|
const timestamp = new Date().toLocaleTimeString()
|
||||||
setLogs(prev => [`[${activeTab}] [${timestamp}] ${message}`, ...prev.slice(0, 9)])
|
setDebugLogs(prev => [...prev.slice(-9), `[${timestamp}] ${message}`])
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
// 從 API 響應格式獲取當前卡片資料
|
|
||||||
const flashcardsData = exampleData.data || []
|
|
||||||
const currentCard = flashcardsData[currentCardIndex] || flashcardsData[0]
|
|
||||||
|
|
||||||
// 轉換為組件所需格式
|
|
||||||
const mockCardData = currentCard ? {
|
|
||||||
word: currentCard.word,
|
|
||||||
definition: currentCard.definition,
|
|
||||||
example: currentCard.example,
|
|
||||||
filledQuestionText: currentCard.filledQuestionText,
|
|
||||||
exampleTranslation: currentCard.exampleTranslation,
|
|
||||||
pronunciation: currentCard.pronunciation,
|
|
||||||
synonyms: currentCard.synonyms || [],
|
|
||||||
cefr: currentCard.difficultyLevel || 'A1',
|
|
||||||
translation: currentCard.translation,
|
|
||||||
// 從 flashcardExampleImages 提取圖片URL
|
|
||||||
exampleImage: currentCard.flashcardExampleImages?.[0]?.exampleImage ?
|
|
||||||
`http://localhost:5008/images/examples/${currentCard.flashcardExampleImages[0].exampleImage.relativePath}` :
|
|
||||||
undefined
|
|
||||||
} : {
|
|
||||||
word: "loading...",
|
|
||||||
definition: "Loading...",
|
|
||||||
example: "Loading...",
|
|
||||||
filledQuestionText: undefined,
|
|
||||||
exampleTranslation: "載入中...",
|
|
||||||
pronunciation: "",
|
|
||||||
synonyms: [],
|
|
||||||
cefr: "A1",
|
|
||||||
translation: "載入中",
|
|
||||||
exampleImage: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 選項題選項 - 從資料中生成
|
|
||||||
const generateVocabChoiceOptions = () => {
|
|
||||||
if (!currentCard) return ["loading"]
|
|
||||||
const correctAnswer = currentCard.word
|
|
||||||
const otherWords = flashcardsData
|
|
||||||
.filter(card => card.word !== correctAnswer)
|
|
||||||
.slice(0, 3)
|
|
||||||
.map(card => card.word)
|
|
||||||
return [correctAnswer, ...otherWords].sort(() => Math.random() - 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
const vocabChoiceOptions = generateVocabChoiceOptions()
|
|
||||||
|
|
||||||
// 回調函數
|
|
||||||
const handleConfidenceSubmit = (level: number) => {
|
|
||||||
addLog(`FlipMemoryTest: 信心等級 ${level}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAnswer = (answer: string) => {
|
|
||||||
addLog(`答案提交: ${answer}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleReportError = () => {
|
|
||||||
addLog('回報錯誤')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageClick = (image: string) => {
|
|
||||||
addLog(`圖片點擊: ${image}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||||
{/* 頁面標題 */}
|
<h1 className="text-3xl font-bold text-gray-900 mb-8">複習系統設計工具</h1>
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Review 組件設計</h1>
|
|
||||||
<p className="text-gray-600">所有 review-tests 組件的 UI 設計頁面</p>
|
|
||||||
|
|
||||||
{/* 卡片切換控制 */}
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
||||||
<div className="mt-4 flex items-center gap-4">
|
{/* 左側:控制面板 */}
|
||||||
<button
|
<div className="xl:col-span-1">
|
||||||
onClick={() => setCurrentCardIndex(Math.max(0, currentCardIndex - 1))}
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||||
disabled={currentCardIndex === 0}
|
<h2 className="text-xl font-semibold mb-6">控制面板</h2>
|
||||||
className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
|
|
||||||
>
|
|
||||||
上一張
|
|
||||||
</button>
|
|
||||||
<span className="text-sm text-gray-600">
|
|
||||||
卡片 {currentCardIndex + 1} / {flashcardsData.length} - {currentCard?.word || 'loading'}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentCardIndex(Math.min(flashcardsData.length - 1, currentCardIndex + 1))}
|
|
||||||
disabled={currentCardIndex >= flashcardsData.length - 1}
|
|
||||||
className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
|
|
||||||
>
|
|
||||||
下一張
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab 導航 */}
|
{/* 資料管理區 */}
|
||||||
<div className="mb-8">
|
<div className="mb-6">
|
||||||
<div className="border-b border-gray-200">
|
<h3 className="font-medium text-gray-900 mb-3">📁 測試資料管理</h3>
|
||||||
<div className="flex space-x-8 overflow-x-auto">
|
<div className="space-y-3">
|
||||||
{testComponents.map((component) => (
|
<select
|
||||||
<button
|
value={currentDataset}
|
||||||
key={component.id}
|
onChange={(e) => setCurrentDataset(e.target.value)}
|
||||||
onClick={() => setActiveTab(component.id)}
|
className="w-full p-2 border rounded-lg"
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors ${
|
>
|
||||||
activeTab === component.id
|
<option value="">選擇測試資料集...</option>
|
||||||
? 'border-blue-500 text-blue-600'
|
{Object.entries(TEST_DATASETS).map(([key, dataset]) => (
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
<option key={key} value={key}>
|
||||||
}`}
|
{dataset.name}
|
||||||
>
|
</option>
|
||||||
{component.name}
|
))}
|
||||||
</button>
|
</select>
|
||||||
))}
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => importDataset(currentDataset)}
|
||||||
|
disabled={!currentDataset}
|
||||||
|
className="flex-1 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
匯入資料
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={resetAllStores}
|
||||||
|
className="px-3 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentDataset && (
|
||||||
|
<div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
{TEST_DATASETS[currentDataset as keyof typeof TEST_DATASETS].description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 模擬控制區 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="font-medium text-gray-900 mb-3">🎮 模擬控制</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
onClick={startSimulation}
|
||||||
|
disabled={!currentDataset || isSimulating}
|
||||||
|
className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSimulating ? '模擬進行中...' : '開始複習模擬'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isSimulating && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsSimulating(false)
|
||||||
|
addLog('⏸️ 模擬已暫停')
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
||||||
|
>
|
||||||
|
暫停模擬
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Store 狀態監控 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="font-medium text-gray-900 mb-3">📊 Store 狀態</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="p-2 bg-gray-50 rounded">
|
||||||
|
<strong>Session:</strong>
|
||||||
|
<span className="ml-2">
|
||||||
|
{sessionStore.currentCard ? `卡片: ${sessionStore.currentCard.word}` : '無卡片'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-gray-50 rounded">
|
||||||
|
<strong>Queue:</strong>
|
||||||
|
<span className="ml-2">
|
||||||
|
{queueStore.testItems.length}個測驗 | 當前: {queueStore.currentTestIndex}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-gray-50 rounded">
|
||||||
|
<strong>Result:</strong>
|
||||||
|
<span className="ml-2">
|
||||||
|
{resultStore.score.correct}✓ / {resultStore.score.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-gray-50 rounded">
|
||||||
|
<strong>Data:</strong>
|
||||||
|
<span className="ml-2">
|
||||||
|
{dataStore.dueCards.length}張詞卡
|
||||||
|
{dataStore.isLoadingCards && ' (載入中...)'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 調試日誌 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-gray-900 mb-3">🐛 調試日誌</h3>
|
||||||
|
<div className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs font-mono h-40 overflow-y-auto">
|
||||||
|
{debugLogs.length === 0 ? (
|
||||||
|
<div className="text-gray-500">等待操作...</div>
|
||||||
|
) : (
|
||||||
|
debugLogs.map((log, index) => (
|
||||||
|
<div key={index} className="mb-1">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* 右側:複習模擬器 */}
|
||||||
|
<div className="xl:col-span-2">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg">
|
||||||
|
<div className="p-6 border-b">
|
||||||
|
<h2 className="text-xl font-semibold">複習模擬器</h2>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
使用真實的 ReviewRunner 組件和 Store 狀態
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 當前測驗組件展示 */}
|
<div className="p-6">
|
||||||
<div className="mb-8">
|
{!currentDataset ? (
|
||||||
<div className="mb-6">
|
<div className="text-center py-12">
|
||||||
<h2 className="text-2xl font-semibold text-gray-900">{activeTab}</h2>
|
<div className="text-gray-400 text-6xl mb-4">📚</div>
|
||||||
<p className="text-sm text-gray-600 mt-1">{testComponents.find(c => c.id === activeTab)?.name}</p>
|
<h3 className="text-lg font-medium text-gray-900 mb-2">開始設計測試</h3>
|
||||||
</div>
|
<p className="text-gray-600">請先從左側控制面板匯入測試資料</p>
|
||||||
<div>
|
</div>
|
||||||
{/* 條件渲染當前選中的測驗組件 */}
|
) : !isSimulating ? (
|
||||||
{activeTab === 'FlipMemoryTest' && (
|
<div className="text-center py-12">
|
||||||
<FlipMemoryTest
|
<div className="text-blue-400 text-6xl mb-4">🎮</div>
|
||||||
cardData={{
|
<h3 className="text-lg font-medium text-gray-900 mb-2">準備開始模擬</h3>
|
||||||
...mockCardData,
|
<p className="text-gray-600 mb-4">
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
已載入 {TEST_DATASETS[currentDataset as keyof typeof TEST_DATASETS].name}
|
||||||
synonyms: mockCardData.synonyms || []
|
</p>
|
||||||
}}
|
<button
|
||||||
onAnswer={handleAnswer}
|
onClick={startSimulation}
|
||||||
onConfidenceSubmit={handleConfidenceSubmit}
|
className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||||||
onReportError={handleReportError}
|
>
|
||||||
/>
|
開始複習模擬
|
||||||
)}
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{/* 真實進度條 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<ProgressTracker
|
||||||
|
completedTests={queueStore.completedTests}
|
||||||
|
totalTests={queueStore.totalTests}
|
||||||
|
onShowTaskList={() => addLog('📋 顯示任務清單')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeTab === 'VocabChoiceTest' && (
|
{/* 真實 ReviewRunner 組件 */}
|
||||||
<VocabChoiceTest
|
<ReviewRunner />
|
||||||
cardData={{
|
|
||||||
...mockCardData,
|
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
|
||||||
synonyms: mockCardData.synonyms || []
|
|
||||||
}}
|
|
||||||
options={vocabChoiceOptions}
|
|
||||||
onAnswer={handleAnswer}
|
|
||||||
onReportError={handleReportError}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'SentenceFillTest' && (
|
{/* 模擬控制 */}
|
||||||
<SentenceFillTest
|
<div className="mt-6 pt-6 border-t">
|
||||||
cardData={{
|
<div className="flex justify-center gap-3">
|
||||||
...mockCardData,
|
<button
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
onClick={() => {
|
||||||
synonyms: mockCardData.synonyms || []
|
setIsSimulating(false)
|
||||||
}}
|
addLog('⏸️ 模擬已暫停')
|
||||||
onAnswer={handleAnswer}
|
}}
|
||||||
onReportError={handleReportError}
|
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
||||||
/>
|
>
|
||||||
)}
|
暫停模擬
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={resetAllStores}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||||
|
>
|
||||||
|
結束模擬
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeTab === 'SentenceReorderTest' && (
|
{/* 測驗佇列視覺化 */}
|
||||||
<SentenceReorderTest
|
{isSimulating && queueStore.testItems.length > 0 && (
|
||||||
cardData={{
|
<div className="bg-white rounded-lg shadow-lg mt-6 p-6">
|
||||||
...mockCardData,
|
<h3 className="font-medium text-gray-900 mb-4">📋 測驗佇列視覺化</h3>
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
synonyms: mockCardData.synonyms || []
|
{queueStore.testItems.map((test, index) => (
|
||||||
}}
|
<div
|
||||||
exampleImage={mockCardData.exampleImage}
|
key={test.id}
|
||||||
onAnswer={handleAnswer}
|
className={`p-3 rounded-lg border ${
|
||||||
onReportError={handleReportError}
|
index === queueStore.currentTestIndex
|
||||||
onImageClick={handleImageClick}
|
? 'border-blue-500 bg-blue-50'
|
||||||
/>
|
: test.isCompleted
|
||||||
)}
|
? 'border-green-500 bg-green-50'
|
||||||
|
: test.isSkipped
|
||||||
{activeTab === 'VocabListeningTest' && (
|
? 'border-yellow-500 bg-yellow-50'
|
||||||
<VocabListeningTest
|
: test.isIncorrect
|
||||||
cardData={{
|
? 'border-red-500 bg-red-50'
|
||||||
...mockCardData,
|
: 'border-gray-200 bg-gray-50'
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
}`}
|
||||||
synonyms: mockCardData.synonyms || []
|
>
|
||||||
}}
|
<div className="flex justify-between items-start">
|
||||||
options={vocabChoiceOptions}
|
<div>
|
||||||
onAnswer={handleAnswer}
|
<div className="font-medium text-sm">{test.word}</div>
|
||||||
onReportError={handleReportError}
|
<div className="text-xs text-gray-600">{test.testType}</div>
|
||||||
/>
|
</div>
|
||||||
)}
|
<div className="text-xs">
|
||||||
|
{index === queueStore.currentTestIndex && '👆'}
|
||||||
{activeTab === 'SentenceListeningTest' && (
|
{test.isCompleted && '✅'}
|
||||||
<SentenceListeningTest
|
{test.isSkipped && '⏭️'}
|
||||||
cardData={{
|
{test.isIncorrect && '❌'}
|
||||||
...mockCardData,
|
{!test.isCompleted && !test.isSkipped && !test.isIncorrect && index !== queueStore.currentTestIndex && '⏳'}
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
</div>
|
||||||
synonyms: mockCardData.synonyms || []
|
</div>
|
||||||
}}
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
options={vocabChoiceOptions}
|
優先級: {test.priority || 100}
|
||||||
exampleImage={mockCardData.exampleImage}
|
</div>
|
||||||
onAnswer={handleAnswer}
|
</div>
|
||||||
onReportError={handleReportError}
|
))}
|
||||||
onImageClick={handleImageClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === 'SentenceSpeakingTest' && (
|
|
||||||
<SentenceSpeakingTest
|
|
||||||
cardData={{
|
|
||||||
...mockCardData,
|
|
||||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
|
||||||
synonyms: mockCardData.synonyms || []
|
|
||||||
}}
|
|
||||||
exampleImage={mockCardData.exampleImage}
|
|
||||||
onAnswer={handleAnswer}
|
|
||||||
onReportError={handleReportError}
|
|
||||||
onImageClick={handleImageClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 操作日誌區域 */}
|
|
||||||
<div className="mt-8 bg-white rounded-lg shadow p-4">
|
|
||||||
<h3 className="font-semibold text-gray-900 mb-3">操作日誌</h3>
|
|
||||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
|
||||||
{logs.length === 0 ? (
|
|
||||||
<p className="text-gray-500 text-sm">無操作記錄</p>
|
|
||||||
) : (
|
|
||||||
logs.map((log, index) => (
|
|
||||||
<div key={index} className="text-sm text-gray-600 font-mono">
|
|
||||||
{log}
|
|
||||||
</div>
|
</div>
|
||||||
))
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue