Преглед на файлове

优化串词成文弹窗和做题选项

s202226701051 преди 7 месеца
родител
ревизия
70bfdd11f2

+ 79 - 31
word-app/src/app/word-story/word-story.page.html

@@ -213,8 +213,8 @@
     </div>
 
     <!-- 风格选择弹窗 - 多步骤向导 -->
-    <div class="wizard-modal-overlay" *ngIf="showStyleModal">
-      <div class="wizard-modal-container" (click)="$event.stopPropagation()">
+    <div class="wizard-modal-overlay" *ngIf="showStyleModal" (click)="onModalOverlayClick()">
+      <div class="wizard-modal-container">
         
         <!-- 弹窗头部 -->
         <div class="wizard-header">
@@ -230,49 +230,97 @@
 
         <!-- 弹窗内容区 -->
         <div class="wizard-content">
-          <!-- 步骤1:风格 -->
+          <!-- 步骤1:风格(多选,最多3个) -->
           <div class="step-content" *ngIf="currentWizardStep === 0">
-            <label class="step-label">请选择你要创作的风格</label>
-            <input 
-              type="text"
-              [(ngModel)]="wizardData.style"
-              [placeholder]="defaultStyles"
-              class="step-input"
-              (keyup.enter)="nextWizardStep()"
-              autofocus>
+            <label class="step-label">请选择你要创作的风格(最多3个)</label>
+            <div class="options-grid">
+              <div class="option-chip"
+                   *ngFor="let style of styleOptions"
+                   [class.selected]="isStyleSelected(style)"
+                   [class.disabled]="!isStyleSelectable(style)"
+                   (click)="selectStyle(style); $event.stopPropagation()">
+                {{ style }}
+              </div>
+            </div>
+            <div class="other-input-wrapper" *ngIf="showOtherStyleInput">
+              <input
+                type="text"
+                [(ngModel)]="wizardData.otherStyle"
+                placeholder="请输入其他风格"
+                class="step-input other-input"
+                (keyup.enter)="confirmOtherStyle()"
+                (click)="$event.stopPropagation()"
+                autofocus>
+            </div>
+            <div class="other-option"
+                 *ngIf="!showOtherStyleInput"
+                 (click)="showOtherInput('style'); $event.stopPropagation()">
+              <ion-icon name="add-outline"></ion-icon>
+              <span>其它</span>
+            </div>
           </div>
 
           <!-- 步骤2:难度 -->
           <div class="step-content" *ngIf="currentWizardStep === 1">
-            <label class="step-label">请输入短文的难度</label>
-            <input 
-              type="text"
-              [(ngModel)]="wizardData.difficulty"
-              placeholder="例如:小学、初中、高中、大学、雅思..."
-              class="step-input"
-              (keyup.enter)="nextWizardStep()"
-              autofocus>
+            <label class="step-label">请选择短文的难度</label>
+            <div class="options-grid">
+              <div class="option-chip"
+                   *ngFor="let difficulty of difficultyOptions"
+                   [class.selected]="wizardData.difficulty === difficulty && !showOtherDifficultyInput"
+                   (click)="selectDifficulty(difficulty); $event.stopPropagation()">
+                {{ difficulty }}
+              </div>
+            </div>
+            <div class="other-input-wrapper" *ngIf="showOtherDifficultyInput">
+              <input
+                type="text"
+                [(ngModel)]="wizardData.difficulty"
+                placeholder="请输入其他难度"
+                class="step-input other-input"
+                (keyup.enter)="nextWizardStep()"
+                (click)="$event.stopPropagation()"
+                autofocus>
+            </div>
+            <div class="other-option" *ngIf="!showOtherDifficultyInput" (click)="showOtherInput('difficulty'); $event.stopPropagation()">
+              <ion-icon name="add-outline"></ion-icon>
+              <span>其它</span>
+            </div>
           </div>
 
           <!-- 步骤3:体裁 -->
           <div class="step-content" *ngIf="currentWizardStep === 2">
-            <label class="step-label">请输入短文的体裁</label>
-            <input 
-              type="text"
-              [(ngModel)]="wizardData.genre"
-              placeholder="例如:记叙文、说明文、议论文、书信体..."
-              class="step-input"
-              (keyup.enter)="confirmStyle()"
-              autofocus>
+            <label class="step-label">请选择短文的体裁</label>
+            <div class="options-grid">
+              <div class="option-chip"
+                   *ngFor="let genre of genreOptions"
+                   [class.selected]="wizardData.genre === genre && !showOtherGenreInput"
+                   (click)="selectGenre(genre); $event.stopPropagation()">
+                {{ genre }}
+              </div>
+            </div>
+            <div class="other-input-wrapper" *ngIf="showOtherGenreInput">
+              <input
+                type="text"
+                [(ngModel)]="wizardData.genre"
+                placeholder="请输入其他体裁"
+                class="step-input other-input"
+                (keyup.enter)="confirmStyle()"
+                (click)="$event.stopPropagation()"
+                autofocus>
+            </div>
+            <div class="other-option" *ngIf="!showOtherGenreInput" (click)="showOtherInput('genre'); $event.stopPropagation()">
+              <ion-icon name="add-outline"></ion-icon>
+              <span>其它</span>
+            </div>
           </div>
         </div>
 
         <!-- 弹窗底部按钮 -->
-        <div class="wizard-footer">
+        <div class="wizard-footer" (click)="$event.stopPropagation()">
           <!-- 第一步:取消 + 下一步 -->
           <ng-container *ngIf="currentWizardStep === 0">
             <button class="wizard-btn btn-secondary" (click)="closeStyleModal()">取消</button>
-            <button class="wizard-btn btn-primary" 
+            <button class="wizard-btn btn-primary"
                     [disabled]="!canProceedWizard"
                     (click)="nextWizardStep()">下一步</button>
           </ng-container>
@@ -280,7 +328,7 @@
           <!-- 中间步骤:上一步 + 下一步 -->
           <ng-container *ngIf="currentWizardStep === 1">
             <button class="wizard-btn btn-secondary" (click)="prevWizardStep()">上一步</button>
-            <button class="wizard-btn btn-primary" 
+            <button class="wizard-btn btn-primary"
                     [disabled]="!canProceedWizard"
                     (click)="nextWizardStep()">下一步</button>
           </ng-container>
@@ -288,7 +336,7 @@
           <!-- 最后一步:上一步 + 开始生成 -->
           <ng-container *ngIf="currentWizardStep === 2">
             <button class="wizard-btn btn-secondary" (click)="prevWizardStep()">上一步</button>
-            <button class="wizard-btn btn-primary btn-highlight" 
+            <button class="wizard-btn btn-primary btn-highlight"
                     [disabled]="!canProceedWizard"
                     (click)="confirmStyle()">开始生成</button>
           </ng-container>

+ 107 - 0
word-app/src/app/word-story/word-story.page.scss

@@ -1090,6 +1090,113 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     min-height: 120px;
   }
 
+  // 选项网格布局
+  .options-grid {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    justify-content: center;
+    margin-bottom: 12px;
+  }
+
+  // 选项按钮样式
+  .option-chip {
+    padding: 10px 18px;
+    border-radius: 12px;
+    border: 2px solid #B3E5FC;
+    background: #E1F5FE;
+    color: #01579B;
+    font-size: 0.95rem;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    display: inline-block;
+    text-align: center;
+
+    &:hover {
+      border-color: #29B6F6;
+      background: #29B6F6;
+      color: white;
+      transform: translateY(-1px);
+    }
+
+    &:active {
+      transform: scale(0.96);
+    }
+
+    &.selected {
+      background: #4FC3F7;
+      color: white;
+      border-color: #4FC3F7;
+      box-shadow: 0 2px 8px rgba(41, 182, 246, 0.25);
+    }
+
+    // 禁用状态:已达到选择上限时
+    &.disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+      pointer-events: none;
+    }
+  }
+
+  // "其它"选项样式
+  .other-option {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 6px;
+    padding: 10px 18px;
+    border-radius: 12px;
+    border: 2px dashed #B0BEC5;
+    background: #FAFAFA;
+    color: #78909C;
+    font-size: 0.95rem;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    margin: 0 auto;
+
+    ion-icon {
+      font-size: 1.2rem;
+    }
+
+    &:hover {
+      border-color: #0288D1;
+      background: #E1F5FE;
+      color: #0288D1;
+      transform: translateY(-1px);
+    }
+
+    &:active {
+      transform: scale(0.96);
+    }
+  }
+
+  // "其它"输入框包装器
+  .other-input-wrapper {
+    margin-top: 12px;
+  }
+
+  .other-input {
+    width: 100%;
+    padding: 12px 16px;
+    border: 2px solid #0288D1;
+    border-radius: 12px;
+    font-size: 1rem;
+    font-weight: 600;
+    color: #333;
+    background: #F0F9FF;
+    transition: all 0.2s ease;
+    outline: none;
+    box-sizing: border-box;
+
+    &:focus {
+      border-color: #01579B;
+      background: white;
+      box-shadow: 0 0 0 3px rgba(1, 87, 155, 0.15);
+    }
+  }
+
   // 退出确认弹窗样式
   .exit-modal-container {
     max-width: 360px;

+ 155 - 10
word-app/src/app/word-story/word-story.page.ts

@@ -9,9 +9,9 @@ import {
   IonIcon 
 } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
-import { 
-  arrowBackOutline, 
-  helpCircleOutline, 
+import {
+  arrowBackOutline,
+  helpCircleOutline,
   checkmark,
   boatOutline,
   informationCircleOutline,
@@ -19,7 +19,8 @@ import {
   chevronBackOutline,
   chevronForwardOutline,
   createOutline,
-  saveOutline
+  saveOutline,
+  addOutline
 } from 'ionicons/icons';
 
 // 单词数据接口
@@ -140,6 +141,16 @@ listSwipeDirection: 'left' | 'right' | null = null;
   // 退出确认弹窗状态
   showExitModal: boolean = false;
 
+  // 预设选项数据
+  styleOptions: string[] = ['童话', '青春', '悬疑', '励志', '科幻', '日常', '历史', '浪漫'];
+  difficultyOptions: string[] = ['小学', '初中', '高中', '大学', '雅思', '托福', '四级', '六级'];
+  genreOptions: string[] = ['记叙', '说明', '议论', '书信'];
+
+  // "其它"输入框显示状态
+  showOtherStyleInput: boolean = false;
+  showOtherDifficultyInput: boolean = false;
+  showOtherGenreInput: boolean = false;
+
   // 生成状态
   isGenerating: boolean = false;
 
@@ -164,15 +175,20 @@ listSwipeDirection: 'left' | 'right' | null = null;
   currentWizardStep: number = 0;
   
   wizardData: {
-    style: string;
+    styles: string[];       // 多选风格,最多3个
+    otherStyle: string;     // "其它"输入的自定义风格
     difficulty: string;
     genre: string;
   } = {
-    style: '',
+    styles: [],
+    otherStyle: '',
     difficulty: '',
     genre: ''
   };
 
+  // 风格多选的最大数量
+  readonly maxStyleCount = 3;
+
   constructor(private router: Router, private wordStoryService: WordStoryService) {
     addIcons({
       arrowBackOutline,
@@ -184,7 +200,8 @@ listSwipeDirection: 'left' | 'right' | null = null;
       chevronBackOutline,
       chevronForwardOutline,
       createOutline,
-      saveOutline
+      saveOutline,
+      addOutline
     });
     // 从其他页通过 state 进入做题页时恢复故事
     const nav = this.router.getCurrentNavigation();
@@ -763,11 +780,132 @@ listSwipeDirection: 'left' | 'right' | null = null;
 
   /** ========== 向导相关方法 ========== */
 
+  // 判断风格是否已选中
+  isStyleSelected(style: string): boolean {
+    return this.wizardData.styles.includes(style);
+  }
+
+  // 选择风格(多选,最多3个)
+  selectStyle(style: string) {
+    const index = this.wizardData.styles.indexOf(style);
+    if (index > -1) {
+      // 已选中,取消选择
+      this.wizardData.styles.splice(index, 1);
+    } else {
+      // 未选中,检查是否可以添加
+      if (this.wizardData.styles.length < this.maxStyleCount) {
+        this.wizardData.styles.push(style);
+      }
+    }
+    // 关闭"其它"输入框并清空内容
+    this.showOtherStyleInput = false;
+    this.wizardData.otherStyle = '';
+  }
+
+  // 判断风格选项是否可选(用于禁用未选中的选项)
+  isStyleSelectable(style: string): boolean {
+    // 如果已选中,总是可以点击(取消选择)
+    if (this.isStyleSelected(style)) {
+      return true;
+    }
+    // 计算当前有效的选择数量:已确认的选项 + "其它"输入框有内容算1个
+    const effectiveCount = this.wizardData.styles.length +
+      (this.showOtherStyleInput && this.wizardData.otherStyle.trim() ? 1 : 0);
+    // 如果未选中,只有未达到上限时才可选
+    return effectiveCount < this.maxStyleCount;
+  }
+
+  // 选择难度
+  selectDifficulty(difficulty: string) {
+    this.wizardData.difficulty = difficulty;
+    this.showOtherDifficultyInput = false;
+  }
+
+  // 选择体裁
+  selectGenre(genre: string) {
+    this.wizardData.genre = genre;
+    this.showOtherGenreInput = false;
+  }
+
+  // 显示"其它"输入框
+  showOtherInput(type: 'style' | 'difficulty' | 'genre') {
+    if (type === 'style') {
+      // 计算当前有效的选择数量:已确认的选项 + "其它"输入框有内容算1个
+      const effectiveCount = this.wizardData.styles.length +
+        (this.showOtherStyleInput && this.wizardData.otherStyle.trim() ? 1 : 0);
+      // 检查是否还有名额添加"其它"
+      if (effectiveCount >= this.maxStyleCount) {
+        return; // 已满,不允许添加
+      }
+      this.showOtherStyleInput = true;
+      this.wizardData.otherStyle = '';
+    } else if (type === 'difficulty') {
+      this.showOtherDifficultyInput = true;
+      this.wizardData.difficulty = '';
+    } else if (type === 'genre') {
+      this.showOtherGenreInput = true;
+      this.wizardData.genre = '';
+    }
+  }
+
+  // 确认"其它"风格输入
+  confirmOtherStyle() {
+    const trimmed = this.wizardData.otherStyle.trim();
+    if (trimmed) {
+      // 检查是否已存在相同风格
+      if (!this.wizardData.styles.includes(trimmed)) {
+        // 如果还没满,可以添加
+        if (this.wizardData.styles.length < this.maxStyleCount) {
+          this.wizardData.styles.push(trimmed);
+        }
+      }
+      this.showOtherStyleInput = false;
+      this.wizardData.otherStyle = '';
+    }
+  }
+
+  // 点击弹窗背景时,隐藏"其它"输入框,回到最初状态
+  onModalOverlayClick() {
+    // 如果任何一个"其它"输入框显示且有内容,不关闭
+    if (this.showOtherStyleInput && this.wizardData.otherStyle.trim()) {
+      return;
+    }
+    if (this.showOtherDifficultyInput && this.wizardData.difficulty.trim()) {
+      return;
+    }
+    if (this.showOtherGenreInput && this.wizardData.genre.trim()) {
+      return;
+    }
+
+    // 隐藏并清除"其它"输入框
+    if (this.showOtherStyleInput) {
+      this.showOtherStyleInput = false;
+      this.wizardData.otherStyle = '';
+    }
+    if (this.showOtherDifficultyInput) {
+      this.showOtherDifficultyInput = false;
+      this.wizardData.difficulty = '';
+    }
+    if (this.showOtherGenreInput) {
+      this.showOtherGenreInput = false;
+      this.wizardData.genre = '';
+    }
+  }
+
   // 检查当前步骤是否可以继续
   get canProceedWizard(): boolean {
     const currentKey = this.wizardSteps[this.currentWizardStep].key;
+
+    // 风格步骤:至少选择1个,最多3个
+    if (currentKey === 'style') {
+      // 已选风格 >= 1,或者 "其它"输入框有内容
+      return this.wizardData.styles.length >= 1 ||
+             (this.showOtherStyleInput && this.wizardData.otherStyle.trim() !== '');
+    }
+
+    // 其他步骤:非空字符串
     const value = this.wizardData[currentKey as keyof typeof this.wizardData];
-    return value !== undefined && value !== null && value.trim() !== '';
+    return typeof value === 'string' && value.trim() !== '';
   }
 
   // 下一步
@@ -797,7 +935,7 @@ listSwipeDirection: 'left' | 'right' | null = null;
 
     // 重置向导状态
     this.currentWizardStep = 0;
-    this.wizardData = { style: '', difficulty: '', genre: '' };
+    this.wizardData = { styles: [], otherStyle: '', difficulty: '', genre: '' };
     this.showStyleModal = true;
   }
 
@@ -805,6 +943,9 @@ listSwipeDirection: 'left' | 'right' | null = null;
   closeStyleModal() {
     this.showStyleModal = false;
     this.currentWizardStep = 0;
+    this.showOtherStyleInput = false;
+    this.showOtherDifficultyInput = false;
+    this.showOtherGenreInput = false;
   }
 
   // 确认风格并生成故事
@@ -822,11 +963,15 @@ listSwipeDirection: 'left' | 'right' | null = null;
     this.isGenerating = true;
     this.storyError = null;
 
+    // 将风格数组转为逗号分隔的字符串
+    const styleString = this.wizardData.styles.join('、');
+
     // 调用AI生成故事
     this.wordStoryService.generateStory(
       selectedWords,
       this.wizardData.difficulty || '高中水平',
-      this.wizardData.genre || '记叙文'
+      this.wizardData.genre || '记叙文',
+      styleString || undefined
     ).subscribe({
       next: (response) => {
         this.storyResponse = response;

+ 123 - 53
word-app/src/app/word-story/word-story.service.ts

@@ -31,92 +31,133 @@ export class WordStoryService {
    * @param words 单词列表
    * @param difficulty 难度描述
    * @param genre 体裁描述
+   * @param style 风格描述(可选)
    */
-  generateStory(words: string[], difficulty: string, genre: string): Observable<StoryResponse> {
-    const systemPrompt = `【角色设定】
-你是一位专业的英语教育内容生成专家,擅长根据指定词汇创作语境丰富、逻辑连贯的英文文章,并设计高质量的完形填空题目。
+  generateStory(words: string[], difficulty: string, genre: string, style?: string): Observable<StoryResponse> {
+    const styleText = style ? `风格:${style}` : '';
+    const systemPrompt = `你是一位专业的英语教育内容生成专家。
 
-【任务流程】
+任务:根据给定的单词创作英文完形填空文章。
 
-第一步:词汇接收与理解
-- 接收用户提供的单词列表(5-10个单词)
-- 分析每个单词的词性、词根、常见派生形式(名词/动词/形容词/副词/复数)
+步骤1:理解单词
+分析每个单词的词性、含义、常用派生形式和近义词。
 
-第二步:文章创作
-【要求】
-- 字数(必达):英文正文 180–260 词。只统计英文单词数,不含中文、标点、空格。
+步骤2:创作文章
+- 字数:180-260 英文单词
+- 难度:${difficulty}
 - 体裁:${genre}
-- 必须自然融入所有指定单词或其派生/近义形式(如 abandon→abandoned/abandonment)
-- 每个目标单词在文中至少出现 1 次,语境真实、情节连贯,难度:${difficulty}
+${styleText}
+- 自然融入所有目标单词或其派生/近义形式
+- 语境真实、逻辑连贯
 
-第三步:标题与挖空
-【标题】为文章生成一个英文标题(titleEn)和一个中文标题(titleZh),简短贴题即可。
+步骤3:生成标题
+英文标题要求:
+- 准确反映文章的核心大意和主要情节
+- 4-9 个英文单词
+- 传递正能量、积极向上的情感
+- 避免使用"A Story"、"The Day"等泛泛词汇
+- 让读者清楚了解文章讲述的主要内容
 
-【挖空规则 - 数量严禁出错】
-- 用户提供 N 个单词,则文中必须且只能挖恰好 N 个空,用 "______"(6 个下划线)标记。严禁挖 N+1 个或更多空,也严禁挖少于 N 个空。
-- 输出前请自检:全文搜索 "______" 的出现次数,必须等于用户提供的单词数量 N。
-- 每个空对应一个正确答案,空与答案一一对应,顺序相同;挖空时优先挖派生形式或近义词,没有再用原型;挖空处上下文要足够。
-- 重要:确保文中的空数 = N,答案数 = N,干扰项数 = 2,总共 N+2 项。
+中文标题要求:
+- 9-16 个汉字
+- 准确反映文章大意
+- 与英文标题对应
+- 传递正能量
 
-第四步:干扰项设计
-【全局共 2 个干扰项】整篇文章只输出 2 个干扰项,干扰项不用挖空。
+步骤4:挖空
+- 用户提供 N 个单词,挖恰好 N 个空,用"______"标记
+- 优先挖派生形式或近义词
+- 每个空对应一个答案,顺序一致
+- 自检:文中"______"数量必须等于 N
 
-【干扰项要求】
-- 干扰项 1:必须是用户给出的某个单词的派生形式或近义词(与任一正确答案不同即可)。
-- 干扰项 2:任意其他合理干扰项(同词性、能填入空位但语义错误,如同场景词、形近词等均可)。
-- 两个干扰项均不得与任一正确答案相同;语法上要能填入空位(词性兼容),语义上在文中错误或与语境不符。
-- 干扰项不要用"______"挖空标记,直接显示单词即可。
+步骤5:设计干扰项
+- 2 个干扰项(全局)
+- 干扰项1:某个答案的派生或近义词
+- 干扰项2:其他合理干扰项(同词性、语义错误)
+- 重要:所有答案必须不同,所有干扰项必须不同,答案和干扰项之间也必须不同
 
-第五步:中文翻译
-- 提供准确、流畅的中文译文
-- 保持段落对应关系
-- 中文翻译中不要用"______"挖空标记,直接显示完整翻译即可
+步骤6:中文翻译
+- 准确流畅的中文译文
+- 保持段落对应
+- 不要用"______",直接翻译完整内容
 
-【输出格式 - 严格JSON】
-
-整体是一个数组,仅两个元素:
+输出格式(严格JSON):
 [
-  {  // 元素一:标题 + 段落
+  {
     "titleEn": "英文标题",
     "titleZh": "中文标题",
     "paragraphs": [
-      {"en": "第一段英文(含______)", "zh": "第一段中文翻译"},
-      {"en": "第二段英文(含______)", "zh": "第二段中文翻译"},
-      ...
+      {"en": "段落英文(含______)", "zh": "段落中文(不含______)"}
     ]
   },
-  {  // 元素二:分开放置答案和干扰项
-    "answers": ["第1空正确答案", "第2空正确答案", "... 共 N 项"],
+  {
+    "answers": ["答案1", "答案2", "...共N项"],
     "distractors": ["干扰项1", "干扰项2"]
   }
 ]
 
-【执行指令】
-1. 英文正文 180–260 词;必须包含 titleEn、titleZh 和 paragraphs。
-2. 文中 "______" 出现次数 = 用户提供的单词数量 N;answers 数组有 N 项,distractors 数组有 2 项。
-3. answers 中的每一项与文中的每一个空一一对应(顺序相同)。
-4. distractors 中的 2 项为全局干扰项。
-5. 输出为合法 JSON,不要任何说明文字。
-6. 若自检发现:文中 "______" 数量 ≠ N,或 answers 数量 ≠ N,或 distractors 数量 ≠ 2,必须丢弃本次结果,重新严格按以上规则完整生成后再输出最终 JSON。`;
+自检清单(输出前必须检查):
+- 英文正文字数 180-260 词 ✓
+- 标题准确反映文章大意 ✓
+- 文中"______"数量 = N ✓
+- answers数量 = N ✓
+- distractors数量 = 2 ✓
+- answers中无重复 ✓
+- distractors中无重复 ✓
+- answers和distractors之间无重复 ✓
+- 中文翻译无"______" ✓
+
+如果任何一项不符合,必须重新生成完整的正确结果后再输出JSON。只输出JSON,不要任何说明文字。`;
 
-    const userPrompt = this.buildPrompt(words, difficulty, genre);
+    const userPrompt = this.buildPrompt(words, difficulty, genre, style);
     
     return this.deepseekService.ask(userPrompt, systemPrompt, {
-      timeout: 120000,
+      timeout: 240000,  // 增加超时时间从180秒到240秒,给AI更多时间
       maxTokens: 4096
     }).pipe(
       map(response => this.parseResponse(response, words.length)),
-      // 若解析或校验失败,最多自动重试 2 次
-      retry(2)
+      // 若解析或校验失败,最多自动重试 7 次(从5次增加到7次)
+      retry(7)
     );
   }
 
   /**
    * 构建提示词
    */
-  private buildPrompt(words: string[], difficulty: string, genre: string): string {
+  private buildPrompt(words: string[], difficulty: string, genre: string, style?: string): string {
     const n = words.length;
-    return `请用以下 ${n} 个单词生成完形填空文章:${words.join('、')}。要求:难度"${difficulty}",体裁"${genre}";英文正文 180–260 词;挖空优先挖派生/近义词;文中恰好挖 ${n} 个空,answers 数组有 ${n} 项,distractors 数组有 2 项。特别强调:必须保证文中 "______" 出现次数 = ${n},answers 数组长度 = ${n},distractors 数组长度 = 2,如发现不符合必须丢弃并重新完整生成后再给出最终 JSON;干扰项不要用"______"挖空标记,直接显示单词;中文翻译中不要用"______"挖空标记,直接给出完整翻译。`;
+    const styleText = style ? `\n- 风格:${style}` : '';
+    return `请用以下 ${n} 个单词生成完形填空文章:${words.join('、')}。
+
+基本要求:
+- 难度:${difficulty}
+- 体裁:${genre}${styleText}
+- 英文正文:180–260 词
+- 标题:5-10个英文单词,准确反映文章大意
+- 文中挖空数:${n} 个
+- 答案数:${n} 个
+- 干扰项数:2 个
+
+关键规则:
+1. 标题要准确反映文章主要内容,不要泛泛而谈
+2. 挖空优先使用派生形式或近义词
+3. answers必须全部不同,distractors也必须不同,answers和distractors之间也不能相同
+4. 中文翻译不要用"______",直接给出完整翻译
+
+输出格式:
+[
+  {
+    "titleEn": "英文标题",
+    "titleZh": "中文标题",
+    "paragraphs": [{"en": "第一段", "zh": "第一段中文"}, ...]
+  },
+  {
+    "answers": ["答案1", "答案2", ...],
+    "distractors": ["干扰1", "干扰2"]
+  }
+]
+
+注意:如果发现不符合要求,必须重新生成完整的正确结果再输出JSON。`;
   }
 
   /**
@@ -181,11 +222,12 @@ export class WordStoryService {
         distractors: Array.isArray(distractors) ? distractors : []
       };
 
-      // === 一致性校验:空的数量、答案数量、干扰项数量 ===
+      // === 一致性校验:空的数量、答案数量、干扰项数量、重复检查 ===
       const blankCount = paragraphs.reduce(
         (s, p) => s + (p.en.match(/______/g) || []).length,
         0
       );
+      
       if (
         blankCount !== expectedBlankCount ||
         answers.length !== expectedBlankCount ||
@@ -196,6 +238,27 @@ export class WordStoryService {
         );
       }
 
+      // === 重复检查:确保 answers 和 distractors 中没有重复 ===
+      const allWords = [...answers, ...distractors];
+      const uniqueWords = new Set(allWords.map(w => w.toLowerCase()));
+      
+      if (allWords.length !== uniqueWords.size) {
+        // 找出重复的单词
+        const duplicates: string[] = [];
+        const seen = new Set<string>();
+        allWords.forEach(word => {
+          const lowerWord = word.toLowerCase();
+          if (seen.has(lowerWord)) {
+            duplicates.push(word);
+          }
+          seen.add(lowerWord);
+        });
+        
+        throw new Error(
+          `Duplicate words found: ${duplicates.join(', ')}. All words must be unique.`
+        );
+      }
+
       return story;
       
     } catch (error) {
@@ -204,3 +267,10 @@ export class WordStoryService {
     }
   }
 }
+
+
+
+
+
+
+