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:
parent
3b6b52c0d4
commit
f08d798aa4
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
@ -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 已移除
|
||||||
|
|
|
||||||
1346
backend/DramaLing.Api/Migrations/20251007093605_FixSynonymsColumn.Designer.cs
generated
Normal file
1346
backend/DramaLing.Api/Migrations/20251007093605_FixSynonymsColumn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue