feat: 修復 AI 生成同義詞完整保存功能

- 添加 Synonyms 屬性到 Flashcard 實體模型並配置 DbContext
- 執行 FixSynonymsColumn migration 在資料庫中添加 synonyms 欄位
- 更新前後端 CreateFlashcardRequest DTO 支援同義詞傳輸
- 修復前端 generate page 包含 AI 生成的同義詞資料
- 添加前端安全 JSON 解析,正確顯示同義詞標籤
- 修復完整資料流程:AI 分析 → 前端處理 → API 傳輸 → 資料庫儲存

現在 AI 生成的同義詞不再被浪費,完整保存並在詞卡詳細頁面顯示。

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-07 18:09:06 +08:00
parent 3b6b52c0d4
commit f08d798aa4
10 changed files with 1409 additions and 3 deletions

View File

@ -62,6 +62,7 @@ public class FlashcardsController : BaseController
f.Example, f.Example,
f.ExampleTranslation, f.ExampleTranslation,
f.IsFavorite, f.IsFavorite,
f.Synonyms,
DifficultyLevelNumeric = f.DifficultyLevelNumeric, DifficultyLevelNumeric = f.DifficultyLevelNumeric,
CEFR = CEFRHelper.ToString(f.DifficultyLevelNumeric), CEFR = CEFRHelper.ToString(f.DifficultyLevelNumeric),
f.CreatedAt, f.CreatedAt,
@ -117,6 +118,7 @@ public class FlashcardsController : BaseController
Pronunciation = request.Pronunciation, Pronunciation = request.Pronunciation,
Example = request.Example, Example = request.Example,
ExampleTranslation = request.ExampleTranslation, ExampleTranslation = request.ExampleTranslation,
Synonyms = request.Synonyms, // 儲存 AI 生成的同義詞
DifficultyLevelNumeric = CEFRHelper.ToNumeric(request.CEFR ?? "A0"), DifficultyLevelNumeric = CEFRHelper.ToNumeric(request.CEFR ?? "A0"),
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow
@ -168,6 +170,7 @@ public class FlashcardsController : BaseController
flashcard.Example, flashcard.Example,
flashcard.ExampleTranslation, flashcard.ExampleTranslation,
flashcard.IsFavorite, flashcard.IsFavorite,
flashcard.Synonyms,
DifficultyLevelNumeric = flashcard.DifficultyLevelNumeric, DifficultyLevelNumeric = flashcard.DifficultyLevelNumeric,
CEFR = CEFRHelper.ToString(flashcard.DifficultyLevelNumeric), CEFR = CEFRHelper.ToString(flashcard.DifficultyLevelNumeric),
flashcard.CreatedAt, flashcard.CreatedAt,
@ -402,5 +405,6 @@ public class CreateFlashcardRequest
public string Pronunciation { get; set; } = string.Empty; public string Pronunciation { get; set; } = string.Empty;
public string Example { get; set; } = string.Empty; public string Example { get; set; } = string.Empty;
public string? ExampleTranslation { get; set; } public string? ExampleTranslation { get; set; }
public string? Synonyms { get; set; } // AI 生成的同義詞 (JSON 字串)
public string? CEFR { get; set; } = string.Empty; public string? CEFR { get; set; } = string.Empty;
} }

View File

@ -129,6 +129,7 @@ public class DramaLingDbContext : DbContext
flashcardEntity.Property(f => f.Pronunciation).HasColumnName("pronunciation"); flashcardEntity.Property(f => f.Pronunciation).HasColumnName("pronunciation");
flashcardEntity.Property(f => f.Example).HasColumnName("example"); flashcardEntity.Property(f => f.Example).HasColumnName("example");
flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation"); flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation");
flashcardEntity.Property(f => f.Synonyms).HasColumnName("synonyms");
// 已刪除的復習相關屬性配置 // 已刪除的復習相關屬性配置
// EasinessFactor, IntervalDays, NextReviewDate, MasteryLevel, // EasinessFactor, IntervalDays, NextReviewDate, MasteryLevel,
// TimesReviewed, TimesCorrect, LastReviewedAt 已移除 // TimesReviewed, TimesCorrect, LastReviewedAt 已移除

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DramaLing.Api.Migrations
{
/// <inheritdoc />
public partial class FixSynonymsColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "synonyms",
table: "flashcards",
type: "TEXT",
maxLength: 2000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "synonyms",
table: "flashcards");
}
}
}

View File

@ -348,6 +348,11 @@ namespace DramaLing.Api.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("pronunciation"); .HasColumnName("pronunciation");
b.Property<string>("Synonyms")
.HasMaxLength(2000)
.HasColumnType("TEXT")
.HasColumnName("synonyms");
b.Property<string>("Translation") b.Property<string>("Translation")
.IsRequired() .IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")

View File

@ -34,6 +34,9 @@ public class Flashcard
public string? ExampleTranslation { get; set; } public string? ExampleTranslation { get; set; }
[MaxLength(2000)]
public string? Synonyms { get; set; }
// 基本狀態 // 基本狀態
public bool IsFavorite { get; set; } = false; public bool IsFavorite { get; set; } = false;

View File

@ -202,6 +202,7 @@ function GenerateContent() {
partOfSpeech: analysis.partOfSpeech || analysis.PartOfSpeech || 'noun', partOfSpeech: analysis.partOfSpeech || analysis.PartOfSpeech || 'noun',
example: analysis.example || `Example sentence with ${word}.`, // 使用分析結果的例句 example: analysis.example || `Example sentence with ${word}.`, // 使用分析結果的例句
exampleTranslation: analysis.exampleTranslation, exampleTranslation: analysis.exampleTranslation,
synonyms: analysis.synonyms ? JSON.stringify(analysis.synonyms) : undefined, // 轉換為 JSON 字串
cefr: cefrValue cefr: cefrValue
} }

View File

@ -22,6 +22,23 @@ export const FlashcardContentBlocks: React.FC<FlashcardContentBlocksProps> = ({
generationProgress, generationProgress,
onGenerateImage onGenerateImage
}) => { }) => {
// 安全解析同義詞 JSON 字串
const parseSynonyms = (synonymsData: any): string[] => {
if (!synonymsData) return []
if (Array.isArray(synonymsData)) return synonymsData
if (typeof synonymsData === 'string') {
try {
const parsed = JSON.parse(synonymsData)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
const synonymsList = parseSynonyms((flashcard as any).synonyms)
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* 翻譯區塊 */} {/* 翻譯區塊 */}
@ -173,11 +190,11 @@ export const FlashcardContentBlocks: React.FC<FlashcardContentBlocksProps> = ({
</div> </div>
{/* 同義詞區塊 */} {/* 同義詞區塊 */}
{(flashcard as any).synonyms && (flashcard as any).synonyms.length > 0 && ( {synonymsList.length > 0 && (
<div className="bg-purple-50 rounded-lg p-4 border border-purple-200"> <div className="bg-purple-50 rounded-lg p-4 border border-purple-200">
<h3 className="font-semibold text-purple-900 mb-3 text-left"></h3> <h3 className="font-semibold text-purple-900 mb-3 text-left"></h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{(flashcard as any).synonyms.map((synonym: string, index: number) => ( {synonymsList.map((synonym: string, index: number) => (
<span <span
key={index} key={index}
className="bg-white text-purple-700 px-3 py-1 rounded-full text-sm border border-purple-200 font-medium" className="bg-white text-purple-700 px-3 py-1 rounded-full text-sm border border-purple-200 font-medium"

View File

@ -29,7 +29,6 @@ export const SearchControls: React.FC<SearchControlsProps> = ({
className="text-sm border border-gray-300 rounded-md px-3 py-1 focus:ring-2 focus:ring-primary focus:border-primary" className="text-sm border border-gray-300 rounded-md px-3 py-1 focus:ring-2 focus:ring-primary focus:border-primary"
> >
<option value="createdAt"></option> <option value="createdAt"></option>
<option value="masteryLevel"></option>
<option value="word"></option> <option value="word"></option>
<option value="cefr">CEFR等級</option> <option value="cefr">CEFR等級</option>
<option value="timesReviewed"></option> <option value="timesReviewed"></option>

View File

@ -43,6 +43,7 @@ export interface CreateFlashcardRequest {
partOfSpeech: string; partOfSpeech: string;
example: string; example: string;
exampleTranslation?: string; exampleTranslation?: string;
synonyms?: string; // AI 生成的同義詞 (JSON 字串格式)
cefr?: string; // A1, A2, B1, B2, C1, C2 cefr?: string; // A1, A2, B1, B2, C1, C2
} }