dramaling-vocab-learning/frontend/components/GrammarCorrectionPanel.tsx

273 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState } from 'react'
interface GrammarCorrection {
hasErrors: boolean
originalText: string
correctedText: string | null
corrections: Array<{
position: { start: number; end: number }
errorType: string
original: string
corrected: string
reason: string
severity: 'high' | 'medium' | 'low'
}>
confidenceScore: number
}
interface GrammarCorrectionPanelProps {
correction: GrammarCorrection
onAcceptCorrection: () => void
onRejectCorrection: () => void
onManualEdit?: (text: string) => void
}
export function GrammarCorrectionPanel({
correction,
onAcceptCorrection,
onRejectCorrection,
onManualEdit
}: GrammarCorrectionPanelProps) {
const [isExpanded, setIsExpanded] = useState(true)
if (!correction.hasErrors) {
return (
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
<div className="flex items-center gap-2">
<div className="text-green-600 text-lg"></div>
<div>
<div className="font-medium text-green-800"></div>
<div className="text-sm text-green-700">
</div>
</div>
</div>
</div>
)
}
const renderHighlightedText = (text: string, corrections: typeof correction.corrections) => {
if (corrections.length === 0) return text
let result: React.ReactNode[] = []
let lastIndex = 0
corrections.forEach((corr, index) => {
// 添加錯誤前的正常文字
if (corr.position.start > lastIndex) {
result.push(
<span key={`normal-${index}`}>
{text.slice(lastIndex, corr.position.start)}
</span>
)
}
// 添加錯誤文字(紅色標記)
result.push(
<span
key={`error-${index}`}
className="relative bg-red-100 border-b-2 border-red-400 px-1 rounded"
title={`錯誤:${corr.reason}`}
>
{corr.original}
<span className="absolute -top-1 -right-1 text-xs text-red-600"></span>
</span>
)
lastIndex = corr.position.end
})
// 添加最後剩餘的正常文字
if (lastIndex < text.length) {
result.push(
<span key="final">
{text.slice(lastIndex)}
</span>
)
}
return <>{result}</>
}
const renderCorrectedText = (text: string, corrections: typeof correction.corrections) => {
if (corrections.length === 0 || !text) return text
let result: React.ReactNode[] = []
let lastIndex = 0
let offset = 0 // 修正後文字長度變化的偏移量
corrections.forEach((corr, index) => {
const adjustedStart = corr.position.start + offset
const originalLength = corr.original.length
const correctedLength = corr.corrected.length
// 添加修正前的正常文字
if (adjustedStart > lastIndex) {
result.push(
<span key={`normal-${index}`}>
{text.slice(lastIndex, adjustedStart)}
</span>
)
}
// 添加修正後的文字(綠色標記)
result.push(
<span
key={`corrected-${index}`}
className="relative bg-green-100 border-b-2 border-green-400 px-1 rounded font-medium"
title={`修正:${corr.reason}`}
>
{corr.corrected}
<span className="absolute -top-1 -right-1 text-xs text-green-600"></span>
</span>
)
lastIndex = adjustedStart + correctedLength
offset += (correctedLength - originalLength)
})
// 添加最後剩餘的正常文字
if (lastIndex < text.length) {
result.push(
<span key="final">
{text.slice(lastIndex)}
</span>
)
}
return <>{result}</>
}
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
return 'bg-red-100 text-red-700 border-red-300'
case 'medium':
return 'bg-yellow-100 text-yellow-700 border-yellow-300'
case 'low':
return 'bg-blue-100 text-blue-700 border-blue-300'
default:
return 'bg-gray-100 text-gray-700 border-gray-300'
}
}
return (
<div className="bg-white border border-red-200 rounded-lg shadow-sm mb-6">
{/* 標題區 */}
<div className="bg-red-50 px-6 py-4 border-b border-red-200 rounded-t-lg">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="text-red-600 text-xl"></div>
<div>
<h3 className="font-semibold text-red-800">
{correction.corrections.length}
</h3>
<div className="text-sm text-red-700">
</div>
</div>
</div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-red-600 hover:text-red-800"
>
{isExpanded ? '收起' : '展開'}
</button>
</div>
</div>
{isExpanded && (
<div className="p-6 space-y-6">
{/* 原始句子 */}
<div>
<h4 className="font-medium text-gray-800 mb-2">📝 </h4>
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="text-lg leading-relaxed">
{renderHighlightedText(correction.originalText, correction.corrections)}
</div>
</div>
</div>
{/* 修正建議 */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-medium text-green-800 mb-3 flex items-center gap-2">
<span className="text-lg">🔧</span>
</h4>
<div className="p-4 bg-white rounded-lg border border-green-300 mb-4">
<div className="text-lg leading-relaxed">
{correction.correctedText && renderCorrectedText(correction.correctedText, correction.corrections)}
</div>
</div>
{/* 修正說明列表 */}
<div className="space-y-3">
<h5 className="font-medium text-green-800">📋 </h5>
{correction.corrections.map((corr, index) => (
<div
key={index}
className={`p-3 rounded-lg border ${getSeverityColor(corr.severity)}`}
>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-white flex items-center justify-center text-sm font-bold">
{index + 1}
</div>
<div className="flex-1">
<div className="font-medium mb-1">
"{corr.original}" "{corr.corrected}"
</div>
<div className="text-sm">
{corr.reason}
</div>
<div className="text-xs mt-1 opacity-75">
{corr.errorType} | {corr.severity}
</div>
</div>
</div>
</div>
))}
</div>
{/* 信心度 */}
<div className="mt-4 text-sm text-green-700">
🎯 {(correction.confidenceScore * 100).toFixed(1)}%
</div>
</div>
{/* 操作按鈕 */}
<div className="flex gap-4">
<button
onClick={onAcceptCorrection}
className="flex-1 bg-green-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
>
<span className="text-lg"></span>
使
</button>
<button
onClick={onRejectCorrection}
className="flex-1 bg-gray-200 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-300 transition-colors flex items-center justify-center gap-2"
>
<span className="text-lg"></span>
</button>
</div>
{/* 學習提醒 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<div className="text-blue-600 text-lg">💡</div>
<div className="text-sm text-blue-800">
<strong></strong>
使
</div>
</div>
</div>
</div>
)}
</div>
)
}