feat: 實現智能例句圖片顯示和AI生成按鈕
- 修改例句圖片邏輯,只顯示已確認存在的圖片 - 為沒有例句圖片的詞彙顯示「新增例句圖」按鈕 - 添加AI生成例句圖片的預留接口 - 提供直觀的視覺提示,區分已有圖片和待生成圖片 - 為下階段AI生成流程做準備 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e424c04443
commit
acd20e3f2c
|
|
@ -24,35 +24,29 @@ function FlashcardsContent() {
|
||||||
const [searchState, searchActions] = useFlashcardSearch(activeTab)
|
const [searchState, searchActions] = useFlashcardSearch(activeTab)
|
||||||
|
|
||||||
// 例句圖片邏輯
|
// 例句圖片邏輯
|
||||||
const getExampleImage = (word: string): string => {
|
const getExampleImage = (word: string): string | null => {
|
||||||
const availableImages = [
|
|
||||||
'/images/examples/bring_up.png',
|
|
||||||
'/images/examples/instinct.png',
|
|
||||||
'/images/examples/warrant.png'
|
|
||||||
]
|
|
||||||
|
|
||||||
const imageMap: {[key: string]: string} = {
|
const imageMap: {[key: string]: string} = {
|
||||||
'brought': '/images/examples/bring_up.png',
|
'brought': '/images/examples/bring_up.png',
|
||||||
'instincts': '/images/examples/instinct.png',
|
'instincts': '/images/examples/instinct.png',
|
||||||
'warrants': '/images/examples/warrant.png',
|
'warrants': '/images/examples/warrant.png',
|
||||||
'hello': '/images/examples/bring_up.png',
|
'evidence': '/images/examples/bring_up.png',
|
||||||
'beautiful': '/images/examples/instinct.png',
|
'recovering': '/images/examples/instinct.png'
|
||||||
'understand': '/images/examples/warrant.png',
|
|
||||||
'elaborate': '/images/examples/bring_up.png',
|
|
||||||
'sophisticated': '/images/examples/instinct.png',
|
|
||||||
'ubiquitous': '/images/examples/warrant.png'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根據詞彙返回對應圖片,如果沒有則根據字母分配
|
// 只返回已確認存在的圖片,沒有則返回 null
|
||||||
const mappedImage = imageMap[word?.toLowerCase()]
|
return imageMap[word?.toLowerCase()] || null
|
||||||
if (mappedImage) return mappedImage
|
}
|
||||||
|
|
||||||
// 根據首字母分配圖片
|
// 檢查詞彙是否有例句圖片
|
||||||
const firstChar = (word || 'a')[0].toLowerCase()
|
const hasExampleImage = (word: string): boolean => {
|
||||||
const charCode = firstChar.charCodeAt(0) - 97 // a=0, b=1, c=2...
|
return getExampleImage(word) !== null
|
||||||
const imageIndex = charCode % availableImages.length
|
}
|
||||||
|
|
||||||
return availableImages[imageIndex]
|
// 處理AI生成例句圖片 (預留接口)
|
||||||
|
const handleGenerateExampleImage = (card: Flashcard) => {
|
||||||
|
console.log('準備為詞彙生成例句圖片:', card.word)
|
||||||
|
// TODO: 下階段實現AI生成流程
|
||||||
|
// router.push(`/generate-image?word=${encodeURIComponent(card.word)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化數據載入
|
// 初始化數據載入
|
||||||
|
|
@ -258,6 +252,8 @@ function FlashcardsContent() {
|
||||||
getCEFRColor={getCEFRColor}
|
getCEFRColor={getCEFRColor}
|
||||||
highlightSearchTerm={highlightSearchTerm}
|
highlightSearchTerm={highlightSearchTerm}
|
||||||
getExampleImage={getExampleImage}
|
getExampleImage={getExampleImage}
|
||||||
|
hasExampleImage={hasExampleImage}
|
||||||
|
onGenerateExampleImage={handleGenerateExampleImage}
|
||||||
router={router}
|
router={router}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -469,7 +465,9 @@ interface SearchResultsProps {
|
||||||
onToggleFavorite: (card: Flashcard) => void
|
onToggleFavorite: (card: Flashcard) => void
|
||||||
getCEFRColor: (level: string) => string
|
getCEFRColor: (level: string) => string
|
||||||
highlightSearchTerm: (text: string, term: string) => React.ReactNode
|
highlightSearchTerm: (text: string, term: string) => React.ReactNode
|
||||||
getExampleImage: (word: string) => string
|
getExampleImage: (word: string) => string | null
|
||||||
|
hasExampleImage: (word: string) => boolean
|
||||||
|
onGenerateExampleImage: (card: Flashcard) => void
|
||||||
router: any
|
router: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,6 +480,8 @@ function SearchResults({
|
||||||
getCEFRColor,
|
getCEFRColor,
|
||||||
highlightSearchTerm,
|
highlightSearchTerm,
|
||||||
getExampleImage,
|
getExampleImage,
|
||||||
|
hasExampleImage,
|
||||||
|
onGenerateExampleImage,
|
||||||
router
|
router
|
||||||
}: SearchResultsProps) {
|
}: SearchResultsProps) {
|
||||||
if (searchState.flashcards.length === 0) {
|
if (searchState.flashcards.length === 0) {
|
||||||
|
|
@ -521,6 +521,8 @@ function SearchResults({
|
||||||
getCEFRColor={getCEFRColor}
|
getCEFRColor={getCEFRColor}
|
||||||
highlightSearchTerm={highlightSearchTerm}
|
highlightSearchTerm={highlightSearchTerm}
|
||||||
getExampleImage={getExampleImage}
|
getExampleImage={getExampleImage}
|
||||||
|
hasExampleImage={hasExampleImage}
|
||||||
|
onGenerateExampleImage={() => onGenerateExampleImage(card)}
|
||||||
router={router}
|
router={router}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -537,11 +539,13 @@ interface FlashcardItemProps {
|
||||||
onToggleFavorite: () => void
|
onToggleFavorite: () => void
|
||||||
getCEFRColor: (level: string) => string
|
getCEFRColor: (level: string) => string
|
||||||
highlightSearchTerm: (text: string, term: string) => React.ReactNode
|
highlightSearchTerm: (text: string, term: string) => React.ReactNode
|
||||||
getExampleImage: (word: string) => string
|
getExampleImage: (word: string) => string | null
|
||||||
|
hasExampleImage: (word: string) => boolean
|
||||||
|
onGenerateExampleImage: () => void
|
||||||
router: any
|
router: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function FlashcardItem({ card, searchTerm, onEdit, onDelete, onToggleFavorite, getCEFRColor, highlightSearchTerm, getExampleImage, router }: FlashcardItemProps) {
|
function FlashcardItem({ card, searchTerm, onEdit, onDelete, onToggleFavorite, getCEFRColor, highlightSearchTerm, getExampleImage, hasExampleImage, onGenerateExampleImage, router }: FlashcardItemProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 relative">
|
<div className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 relative">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|
@ -554,25 +558,40 @@ function FlashcardItem({ card, searchTerm, onEdit, onDelete, onToggleFavorite, g
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
{/* 例句圖片 */}
|
{/* 例句圖片區域 */}
|
||||||
<div className="w-54 h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center">
|
<div className="w-54 h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center">
|
||||||
<img
|
{hasExampleImage(card.word) ? (
|
||||||
src={getExampleImage(card.word)}
|
// 有例句圖片時顯示圖片
|
||||||
alt={`${card.word} example`}
|
<img
|
||||||
className="w-full h-full object-cover"
|
src={getExampleImage(card.word)!}
|
||||||
onError={(e) => {
|
alt={`${card.word} example`}
|
||||||
const target = e.target as HTMLImageElement
|
className="w-full h-full object-cover"
|
||||||
target.style.display = 'none'
|
onError={(e) => {
|
||||||
target.parentElement!.innerHTML = `
|
const target = e.target as HTMLImageElement
|
||||||
<div class="text-gray-400 text-xs text-center">
|
target.style.display = 'none'
|
||||||
<svg class="w-6 h-6 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
target.parentElement!.innerHTML = `
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
<div class="text-gray-400 text-xs text-center">
|
||||||
</svg>
|
<svg class="w-6 h-6 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
例句圖
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
</div>
|
</svg>
|
||||||
`
|
圖片載入失敗
|
||||||
}}
|
</div>
|
||||||
/>
|
`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// 沒有例句圖片時顯示新增按鈕
|
||||||
|
<button
|
||||||
|
onClick={onGenerateExampleImage}
|
||||||
|
className="w-full h-full flex flex-col items-center justify-center text-gray-500 hover:text-blue-600 hover:bg-blue-50 transition-colors group"
|
||||||
|
title="點擊生成例句圖片"
|
||||||
|
>
|
||||||
|
<svg className="w-8 h-8 mb-2 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-medium">新增例句圖</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 詞卡信息 */}
|
{/* 詞卡信息 */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue